@melaya/runner 1.1.3 → 1.1.4

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.
@@ -78,6 +78,18 @@ export interface BrowserBridge {
78
78
  * (looked up by sessionId directly).
79
79
  */
80
80
  setWatchLease(sessionId: string, active: boolean, framePostUrl: string, runAuthHeader: string): void;
81
+ /**
82
+ * Close one or all interactive sessions (emergency stop / per-session close).
83
+ *
84
+ * When sessionId is provided, only that interactive session is closed.
85
+ * When sessionId is omitted (or undefined), ALL active interactive sessions
86
+ * on this runner are closed.
87
+ *
88
+ * Returns the array of sessionIds that were actually closed. Idempotent;
89
+ * per-session errors are swallowed (best-effort). Does NOT touch
90
+ * grant-scoped run sessions (byRunId).
91
+ */
92
+ killInteractive(sessionId?: string): Promise<string[]>;
81
93
  shutdown(): Promise<void>;
82
94
  }
83
95
  export declare function startBrowserBridge(opts: {
@@ -42,23 +42,26 @@ import { runSandboxedScript } from "./codeWorker.js";
42
42
  // Limits and constants
43
43
  // ---------------------------------------------------------------------
44
44
  // Injected into every interactive-session page to display a non-interactive
45
- // Melaya branding overlay (pill badge + pulsing neon border). Defined once
46
- // here so it is shared between context.addInitScript (persistent across
47
- // navigations) and the one-shot page.evaluate on the already-open page.
45
+ // Melaya branding overlay (pill badge with logo + pulsing cyan border).
46
+ // Defined once here so it is shared between context.addInitScript (persistent
47
+ // across navigations) and the one-shot page.evaluate on the already-open page.
48
+ // Palette: cyan #22D3EE primary accent, indigo #7C6FF0 subtle secondary,
49
+ // slate/navy #0F172A base. Less purple than the prior version.
48
50
  const MELAYA_BRAND_SCRIPT = `(() => {
49
51
  if (window.__melayaBrand) return; window.__melayaBrand = true;
50
52
  const ID = "__melaya_brand";
53
+ const LOGO = "data:image/png;base64,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";
51
54
  const inject = () => {
52
55
  const root = document.documentElement; if (!root) return;
53
56
  if (document.getElementById(ID + "_border")) return;
54
57
  const style = document.createElement("style");
55
58
  style.id = ID + "_style";
56
- style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 10px 2px rgba(109,40,217,.55),inset 0 0 34px 7px rgba(34,211,238,.26)}50%{box-shadow:inset 0 0 20px 5px rgba(139,92,246,.9),inset 0 0 66px 15px rgba(34,211,238,.5)}}@keyframes melBrandDot{0%,100%{opacity:.5;transform:scale(.85)}50%{opacity:1;transform:scale(1.15)}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.4s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font:600 12px/1 -apple-system,Segoe UI,Roboto,sans-serif;color:#E9D5FF;background:linear-gradient(90deg,rgba(30,58,138,.94),rgba(109,40,217,.94));padding:6px 14px;border-radius:999px;letter-spacing:.3px;box-shadow:0 0 16px 2px rgba(139,92,246,.7),0 0 5px rgba(34,211,238,.6);display:flex;align-items:center;gap:7px;backdrop-filter:blur(4px)}#" + ID + "_dot{width:7px;height:7px;border-radius:50%;background:#22D3EE;box-shadow:0 0 8px 2px #22D3EE;animation:melBrandDot 1.5s ease-in-out infinite}";
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:600 12px/1 -apple-system,Segoe UI,Roboto,sans-serif;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))}";
57
60
  (document.head || root).appendChild(style);
58
61
  const border = document.createElement("div"); border.id = ID + "_border";
59
62
  const badge = document.createElement("div"); badge.id = ID + "_badge";
60
- const dot = document.createElement("span"); dot.id = ID + "_dot";
61
- badge.appendChild(dot); badge.appendChild(document.createTextNode("Controlled by Melaya"));
63
+ const logo = document.createElement("img"); logo.id = ID + "_logo"; logo.src = LOGO; logo.alt = "Melaya";
64
+ badge.appendChild(logo); badge.appendChild(document.createTextNode("Controlled by Melaya"));
62
65
  root.appendChild(border); root.appendChild(badge);
63
66
  };
64
67
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
@@ -1206,6 +1209,38 @@ export async function startBrowserBridge(opts) {
1206
1209
  }),
1207
1210
  ]);
1208
1211
  };
1212
+ // killInteractive: close one or all interactive sessions (emergency stop).
1213
+ // Does NOT touch grant-scoped run sessions in byRunId.
1214
+ const killInteractive = async (sessionId) => {
1215
+ // Build the list of sessionIds to close.
1216
+ const targets = sessionId !== undefined
1217
+ ? (interactiveSessions.has(sessionId) ? [sessionId] : [])
1218
+ : [...interactiveSessions.keys()];
1219
+ const closed = [];
1220
+ await Promise.all(targets.map(async (id) => {
1221
+ try {
1222
+ // Stop any active watch-lease frame producer for this session.
1223
+ for (const [sid, wl] of watchLeases) {
1224
+ if (wl.runId === id)
1225
+ stopWatchLease(sid);
1226
+ }
1227
+ // Remove from the interactiveSessions map first so the context
1228
+ // "close" handler does not re-emit a redundant "closed" state.
1229
+ interactiveSessions.delete(id);
1230
+ // Tear down through sessionManager (closes the Playwright context,
1231
+ // cleans up leases, removes the session record).
1232
+ await sessions.teardownRun(id, "kill_interactive").catch(() => { });
1233
+ log(`[browser-kill] interactive session killed: ${id.slice(0, 16)}`);
1234
+ closed.push(id);
1235
+ }
1236
+ catch {
1237
+ // Best-effort per-session: a session that already closed or crashed
1238
+ // should not prevent other sessions from being killed.
1239
+ closed.push(id);
1240
+ }
1241
+ }));
1242
+ return closed;
1243
+ };
1209
1244
  return {
1210
1245
  url: `http://127.0.0.1:${port}`,
1211
1246
  port,
@@ -1218,6 +1253,7 @@ export async function startBrowserBridge(opts) {
1218
1253
  launchInteractive,
1219
1254
  getInteractiveSession,
1220
1255
  setWatchLease,
1256
+ killInteractive,
1221
1257
  async shutdown() {
1222
1258
  // Stop all watch-lease producers before closing sessions.
1223
1259
  for (const sid of [...watchLeases.keys()])
@@ -327,6 +327,39 @@ export async function connect(opts) {
327
327
  }
328
328
  });
329
329
  });
330
+ // ── BrowserControlPage emergency stop / per-session close (browser:kill) ──
331
+ // The server emits browser:kill { sessionId? } when the user presses the
332
+ // emergency-stop button (no sessionId = kill all) or closes a specific
333
+ // session card (sessionId present). The bridge closes the matching
334
+ // Playwright context(s) and returns the sessionIds it actually closed.
335
+ // For each closed session a browser:session { state: "closed" } is emitted
336
+ // so the server marks the DB row closed.
337
+ socket.on("browser:kill", async (payload) => {
338
+ const targetId = payload?.sessionId ? String(payload.sessionId) : undefined;
339
+ if (opts.verbose) {
340
+ console.log(chalk.gray(` [browser-kill] received sessionId=${targetId ? targetId.slice(0, 16) : "(all)"}`));
341
+ }
342
+ try {
343
+ const bridge = await _ensureBrowserBridge();
344
+ if (!bridge) {
345
+ console.log(chalk.yellow(" ! browser:kill: bridge unavailable"));
346
+ return;
347
+ }
348
+ const closedIds = await bridge.killInteractive(targetId);
349
+ for (const sid of closedIds) {
350
+ socket.emit("browser:session", { sessionId: sid, state: "closed", engine: "" });
351
+ if (opts.verbose) {
352
+ console.log(chalk.gray(` [browser-kill] browser:session sessionId=${sid.slice(0, 16)} state=closed`));
353
+ }
354
+ }
355
+ if (closedIds.length > 0) {
356
+ console.log(chalk.gray(` [browser-kill] closed ${closedIds.length} interactive session(s)`));
357
+ }
358
+ }
359
+ catch (e) {
360
+ console.log(chalk.yellow(` ! browser:kill error: ${e?.message || e}`));
361
+ }
362
+ });
330
363
  // ── Heartbeat ──────────────────────────────────────────────────────
331
364
  setInterval(() => {
332
365
  if (socket.connected)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,