relay-companion 0.1.26 → 0.1.28

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/overlay/main.cjs CHANGED
@@ -20,7 +20,7 @@
20
20
  // - Mutations (accept/reject/approve/decline/answer) call RelayClient with device-token
21
21
  // auth. ack/mark-read writes state.json directly (atomic temp+rename).
22
22
 
23
- const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell, screen } = require("electron");
23
+ const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell, screen, systemPreferences } = require("electron");
24
24
  const fs = require("node:fs");
25
25
  const os = require("node:os");
26
26
  const path = require("node:path");
@@ -30,6 +30,11 @@ const { execFile, execFileSync, spawn, pathToFileURL } = (() => {
30
30
  return { execFile: cp.execFile, execFileSync: cp.execFileSync, spawn: cp.spawn, pathToFileURL: url.pathToFileURL };
31
31
  })();
32
32
  const { hostFromBundle, chooseClickHost } = require("./host-select.cjs");
33
+ const {
34
+ reinforceSpacePresence,
35
+ showInactiveOnAllSpaces,
36
+ subscribeActiveSpaceChanges,
37
+ } = require("./space-presence.cjs");
33
38
 
34
39
  const RELAY_HOME = process.env.RELAY_HOME || process.env.RELAY_COMPANION_HOME || path.join(os.homedir(), ".relay-companion");
35
40
  const STATE_PATH = path.join(RELAY_HOME, "state.json");
@@ -884,31 +889,59 @@ function anchorTopRight() {
884
889
  return { x: wa.x + wa.width - WIN.width - MARGIN, y: wa.y + MARGIN, width: WIN.width, height: WIN.height };
885
890
  }
886
891
 
892
+ function placeOverlayWindow({ reposition = true, moveTop = false } = {}) {
893
+ if (!win || win.isDestroyed()) return;
894
+ if (reposition) {
895
+ try {
896
+ win.setBounds(anchorTopRight(), false);
897
+ } catch (error) {
898
+ console.error("[overlay] setBounds failed:", error && error.message);
899
+ }
900
+ }
901
+ reinforceSpacePresence(win, { moveTop });
902
+ }
903
+
887
904
  // Show the overlay window. Electron's forwarded-mousemove stream (the thing that
888
905
  // lets the renderer detect hover on a click-through window) can stall across a
889
906
  // hide()/show() cycle, leaving the window permanently click-through. Re-assert
890
907
  // setIgnoreMouseEvents on every hidden->shown transition and tell the renderer to
891
908
  // reset its interactivity handshake.
892
- function showOverlayWindow() {
893
- if (!win || win.isDestroyed() || win.isVisible()) return;
894
- win.showInactive();
909
+ function showOverlayWindow({ force = false, reposition = true } = {}) {
910
+ if (!win || win.isDestroyed()) return;
911
+ const visible = win.isVisible();
912
+ if (visible && !force) {
913
+ reinforceSpacePresence(win);
914
+ return;
915
+ }
916
+ placeOverlayWindow({ reposition });
917
+ const shown = showInactiveOnAllSpaces(win, { force });
895
918
  win.setIgnoreMouseEvents(true, { forward: true });
896
- win.webContents.send("shown");
919
+ if (shown) win.webContents.send("shown");
897
920
  }
898
921
 
899
- function maybeShow() {
922
+ function maybeShow({ force = false, reposition = true } = {}) {
900
923
  if (!win || win.isDestroyed()) return;
901
924
  // Dismissed keeps the window hidden except while a ghost notification is up.
902
925
  // trayForcedVisible honors an explicit status-area click even when no host runs.
903
926
  const dismissedEffective = trayAvailable && dismissed;
904
927
  const wanted = (hostRunning || trayForcedVisible) && (!dismissedEffective || ghostActive);
905
928
  if (wanted) {
906
- showOverlayWindow();
929
+ showOverlayWindow({ force, reposition });
907
930
  } else if (win.isVisible()) {
908
931
  win.hide();
909
932
  }
910
933
  }
911
934
 
935
+ function refreshOverlayForActiveSpace({ force = false } = {}) {
936
+ if (!win || win.isDestroyed()) return;
937
+ reinforceSpacePresence(win, { moveTop: true });
938
+ readHostProcesses((err, stdout) => {
939
+ if (!err) updateHostRunningFromProcesses(stdout);
940
+ if (hostRunning) trayForcedVisible = false;
941
+ maybeShow({ force, reposition: false });
942
+ });
943
+ }
944
+
912
945
  function readHostProcesses(cb) {
913
946
  if (process.platform === "win32") {
914
947
  execFile("tasklist", ["/FO", "CSV", "/NH"], cb);
@@ -1010,6 +1043,21 @@ function createWindow() {
1010
1043
  setInterval(pollHosts, 1500);
1011
1044
  }
1012
1045
 
1046
+ function installActiveSpaceWatcher() {
1047
+ if (process.platform !== "darwin") return;
1048
+ subscribeActiveSpaceChanges(
1049
+ systemPreferences,
1050
+ () => {
1051
+ // Space transitions finish asynchronously. Force one re-show after the switch
1052
+ // starts and again after the animation settles so macOS cannot leave the
1053
+ // transparent accessory window assigned only to the previous Desktop.
1054
+ setTimeout(() => refreshOverlayForActiveSpace({ force: true }), 120);
1055
+ setTimeout(() => refreshOverlayForActiveSpace({ force: true }), 700);
1056
+ },
1057
+ { log: (message) => console.error("[overlay]", message) },
1058
+ );
1059
+ }
1060
+
1013
1061
  // ---- OS status area (tray/menu bar) ----------------------------------------
1014
1062
 
1015
1063
  // The Relay mark lives in the OS status area: macOS menu bar, Windows system tray.
@@ -1040,8 +1088,8 @@ function createTrayIcon() {
1040
1088
  function showFromTray() {
1041
1089
  setDismissed(false);
1042
1090
  ghostActive = false;
1043
- if (!hostRunning) trayForcedVisible = true; // an explicit click always gets the card
1044
- maybeShow();
1091
+ trayForcedVisible = true; // an explicit click always gets the card
1092
+ maybeShow({ force: true });
1045
1093
  if (win && !win.isDestroyed()) win.webContents.send("openFull");
1046
1094
  }
1047
1095
 
@@ -1117,7 +1165,10 @@ ipcMain.on("relay:interactive", (_e, interactive) => {
1117
1165
  if (win && !win.isDestroyed()) win.setIgnoreMouseEvents(!interactive, { forward: true });
1118
1166
  });
1119
1167
  ipcMain.on("relay:setPos", (_e, x, y) => {
1120
- if (win && !win.isDestroyed() && Number.isFinite(x) && Number.isFinite(y)) win.setPosition(Math.round(x), Math.round(y));
1168
+ if (win && !win.isDestroyed() && Number.isFinite(x) && Number.isFinite(y)) {
1169
+ win.setPosition(Math.round(x), Math.round(y));
1170
+ reinforceSpacePresence(win);
1171
+ }
1121
1172
  });
1122
1173
  // ✕ on the card: hide the overlay entirely until the status-area Relay mark is clicked.
1123
1174
  // The renderer plays its exit animation first, then sends this.
@@ -1176,6 +1227,7 @@ if (!gotSingleInstanceLock) {
1176
1227
  if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
1177
1228
  createWindow();
1178
1229
  createTray();
1230
+ installActiveSpaceWatcher();
1179
1231
  });
1180
1232
 
1181
1233
  app.on("window-all-closed", () => {
@@ -0,0 +1,53 @@
1
+ const ACTIVE_SPACE_CHANGED = "NSWorkspaceActiveSpaceDidChangeNotification";
2
+
3
+ function isUsableWindow(win) {
4
+ return Boolean(win && !(typeof win.isDestroyed === "function" && win.isDestroyed()));
5
+ }
6
+
7
+ function reinforceSpacePresence(win, { moveTop = false } = {}) {
8
+ if (!isUsableWindow(win)) return false;
9
+ try {
10
+ win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
11
+ } catch {}
12
+ try {
13
+ win.setAlwaysOnTop(true, "floating");
14
+ } catch {}
15
+ if (moveTop && typeof win.moveTop === "function") {
16
+ try {
17
+ win.moveTop();
18
+ } catch {}
19
+ }
20
+ return true;
21
+ }
22
+
23
+ function showInactiveOnAllSpaces(win, { force = false } = {}) {
24
+ if (!isUsableWindow(win)) return false;
25
+ const visible = typeof win.isVisible === "function" ? win.isVisible() : false;
26
+ if (visible && !force) {
27
+ reinforceSpacePresence(win);
28
+ return false;
29
+ }
30
+
31
+ reinforceSpacePresence(win);
32
+ if (visible && force && typeof win.hide === "function") win.hide();
33
+ if (typeof win.showInactive === "function") win.showInactive();
34
+ reinforceSpacePresence(win, { moveTop: true });
35
+ return true;
36
+ }
37
+
38
+ function subscribeActiveSpaceChanges(systemPreferences, callback, { log = () => {} } = {}) {
39
+ if (!systemPreferences || typeof systemPreferences.subscribeWorkspaceNotification !== "function") return null;
40
+ try {
41
+ return systemPreferences.subscribeWorkspaceNotification(ACTIVE_SPACE_CHANGED, () => callback());
42
+ } catch (error) {
43
+ log(`space change subscription failed: ${error && error.message ? error.message : String(error)}`);
44
+ return null;
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ ACTIVE_SPACE_CHANGED,
50
+ reinforceSpacePresence,
51
+ showInactiveOnAllSpaces,
52
+ subscribeActiveSpaceChanges,
53
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -172,6 +172,10 @@ export function spawnDetachedUpdate({
172
172
  ` ROOT="$(npm root -g 2>/dev/null)"`,
173
173
  ` PKG="$ROOT/${PACKAGE_NAME}"`,
174
174
  ` ELECTRON="$PKG/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"`,
175
+ ` if [ ! -x "$ELECTRON" ] && [ -f "$PKG/node_modules/electron/install.js" ]; then`,
176
+ ` echo "[relay-update] $(date) electron runtime missing; running electron install.js" >> ${LOG} 2>&1`,
177
+ ` node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true`,
178
+ ` fi`,
175
179
  ` if [ -x "$ELECTRON" ] && [ -f "$PKG/bin/relay.js" ] && grep -q '"version"' "$PKG/package.json" 2>/dev/null; then`,
176
180
  ` echo "[relay-update] $(date) install verified; restarting services" >> ${LOG} 2>&1`,
177
181
  ` /bin/launchctl kickstart -k gui/${uid}/${PILL_LABEL} >> ${LOG} 2>&1 || true`,
@@ -179,10 +183,16 @@ export function spawnDetachedUpdate({
179
183
  ` else`,
180
184
  ` echo "[relay-update] $(date) post-install verify FAILED; restoring ${restore}" >> ${LOG} 2>&1`,
181
185
  ` npm install -g ${PACKAGE_NAME}@${restore} >> ${LOG} 2>&1 || true`,
186
+ ` ROOT="$(npm root -g 2>/dev/null)"`,
187
+ ` PKG="$ROOT/${PACKAGE_NAME}"`,
188
+ ` if [ -f "$PKG/node_modules/electron/install.js" ]; then node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true; fi`,
182
189
  ` fi`,
183
190
  `else`,
184
191
  ` echo "[relay-update] $(date) install FAILED; restoring ${restore}" >> ${LOG} 2>&1`,
185
192
  ` npm install -g ${PACKAGE_NAME}@${restore} >> ${LOG} 2>&1 || true`,
193
+ ` ROOT="$(npm root -g 2>/dev/null)"`,
194
+ ` PKG="$ROOT/${PACKAGE_NAME}"`,
195
+ ` if [ -f "$PKG/node_modules/electron/install.js" ]; then node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true; fi`,
186
196
  `fi`,
187
197
  ].join("\n");
188
198
  const child = spawnImpl("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
package/src/install.js CHANGED
@@ -240,6 +240,30 @@ function electronPathForPackageRoot(packageRoot) {
240
240
  }
241
241
  }
242
242
 
243
+ export function ensureElectronRuntime(packageRoot, { runCommand = run } = {}) {
244
+ let electronPath = electronPathForPackageRoot(packageRoot);
245
+ if (electronPath && fs.existsSync(electronPath)) return { ok: true, electronPath, repaired: false };
246
+
247
+ const installScript = path.join(packageRoot, "node_modules", "electron", "install.js");
248
+ if (!fs.existsSync(installScript)) {
249
+ return { ok: false, reason: "electron_install_script_missing", electronPath, installScript };
250
+ }
251
+
252
+ const res = runCommand(process.execPath, [installScript], { timeoutMs: npmInstallTimeoutMs() });
253
+ electronPath = electronPathForPackageRoot(packageRoot);
254
+ if (res.ok && electronPath && fs.existsSync(electronPath)) {
255
+ return { ok: true, electronPath, repaired: true };
256
+ }
257
+
258
+ return {
259
+ ok: false,
260
+ reason: "electron_runtime_missing",
261
+ electronPath,
262
+ installScript,
263
+ detail: res.out,
264
+ };
265
+ }
266
+
243
267
  /** Whether an agent CLI is on PATH. */
244
268
  export function commandExists(cmd) {
245
269
  const res = spawnSync(cmd, ["--version"], { encoding: "utf8", timeout: 8000 });
@@ -333,10 +357,11 @@ export function installDaemonAutostart(bin = relayBinPath(), node = process.exec
333
357
  export function installPillAutostart(bin = relayBinPath(), { platform = process.platform, runCommand = run } = {}) {
334
358
  if (platform !== "darwin" && platform !== "win32") return { ok: false, reason: "autostart_unsupported_platform" };
335
359
  const packageRoot = packageRootForBin(bin);
336
- const electronPath = electronPathForPackageRoot(packageRoot);
360
+ const electron = ensureElectronRuntime(packageRoot, { runCommand });
361
+ const electronPath = electron.electronPath;
337
362
  const overlayMain = path.join(packageRoot, "overlay", "main.cjs");
338
- if (!electronPath || !fs.existsSync(overlayMain)) {
339
- return { ok: false, reason: "pill_runtime_missing", electronPath, overlayMain };
363
+ if (!electron.ok || !electronPath || !fs.existsSync(overlayMain)) {
364
+ return { ok: false, reason: "pill_runtime_missing", electronPath, overlayMain, electron };
340
365
  }
341
366
  if (platform === "win32") {
342
367
  const logPath = path.join(os.homedir(), ".relay", "pill.log");