relay-companion 0.1.366 → 0.1.367

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.366",
3
+ "version": "0.1.367",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "relay-companion",
9
- "version": "0.1.366",
9
+ "version": "0.1.367",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/claude-agent-sdk": "0.3.195",
@@ -94,7 +94,12 @@
94
94
  Elevation is three tokens: the card's own drop shadow (this one), the
95
95
  folded pill's lighter cousin, and one popover shadow — nothing else casts. */
96
96
  .card {
97
- position:absolute; inset:0;
97
+ /* The native window preserves its current screen-space top-right point while
98
+ it changes size. Anchor the visible sheet to that same point so a folded
99
+ pill never lands at the old left edge and then jumps home when AppKit
100
+ trims the transparent remainder. This remains local to the current native
101
+ window, so a user-dragged pill morphs exactly where it was placed. */
102
+ position:absolute; top:0; right:0;
98
103
  width:344px; height:524px;
99
104
  background:var(--bg);
100
105
  border-radius:var(--r-card);
@@ -2685,15 +2690,25 @@
2685
2690
  const W = { v: EXPANDED.w, vel: 0, t: EXPANDED.w };
2686
2691
  const H = { v: EXPANDED.h, vel: 0, t: EXPANDED.h };
2687
2692
  let raf = null, lastT = 0;
2688
- // Main sizes the native window to this card. Deduped: one IPC per distinct
2689
- // integer size.
2693
+ // Main sizes the native window to this card. A morph has two explicit phases:
2694
+ // prepare grows the native surface before the first destination paint; settled
2695
+ // trims it only after the renderer's spring actually finishes. A monotonic id
2696
+ // makes rapid collapse/expand retargeting reject stale completions.
2690
2697
  let interactive = false;
2691
2698
  let lastSentCardSize = "";
2692
- function publishCardSize(w, h) {
2693
- const key = Math.round(w) + "x" + Math.round(h);
2699
+ let cardMotionId = 0;
2700
+ // A renderer reload starts a fresh ordering domain. Without this session id,
2701
+ // main could mistake the new renderer's low motion ids for stale messages.
2702
+ const cardMotionSessionId = crypto.randomUUID();
2703
+ function publishCardSize(w, h, { phase = "prepare", motionId = cardMotionId } = {}) {
2704
+ const key = motionId + ":" + phase + ":" + Math.round(w) + "x" + Math.round(h);
2694
2705
  if (key === lastSentCardSize) return;
2695
2706
  lastSentCardSize = key;
2696
- if (window.relay.cardSize) window.relay.cardSize(Math.round(w), Math.round(h));
2707
+ if (window.relay.cardSize) window.relay.cardSize(Math.round(w), Math.round(h), {
2708
+ phase,
2709
+ motionId,
2710
+ motionSessionId: cardMotionSessionId,
2711
+ });
2697
2712
  }
2698
2713
  function step(s, dt) {
2699
2714
  const before = s.v - s.t;
@@ -2719,24 +2734,20 @@
2719
2734
  for (let i = 0; i < n; i++) { const a = step(W, h), b = step(H, h); moving = moving || a || b; }
2720
2735
  cardEl.style.width = W.v.toFixed(2) + "px";
2721
2736
  cardEl.style.height = H.v.toFixed(2) + "px";
2722
- // Native resize only needs to track the animation coarsely; sending it every
2723
- // frame would compete with the renderer's own motion.
2724
- publishCardSizeThrottled(W.v, H.v);
2725
2737
  if (moving) raf = requestAnimationFrame(frame);
2726
- else { raf = null; cardEl.style.willChange = ""; publishCardSize(W.t, H.t); }
2727
- }
2728
- let lastPublishAt = 0;
2729
- function publishCardSizeThrottled(w, h) {
2730
- const now = performance.now();
2731
- if (now - lastPublishAt < 80) return;
2732
- lastPublishAt = now;
2733
- publishCardSize(w, h);
2738
+ else {
2739
+ raf = null;
2740
+ cardEl.style.willChange = "";
2741
+ publishCardSize(W.t, H.t, { phase:"settled", motionId:cardMotionId });
2742
+ }
2734
2743
  }
2735
2744
  function springTo(w, h) {
2736
2745
  W.t = w; H.t = h;
2746
+ const motionId = ++cardMotionId;
2737
2747
  // Publish the destination synchronously so growth is available before paint
2738
2748
  // and a delayed animation frame cannot leave a stale native window size.
2739
- publishCardSize(w, h);
2749
+ // Shrink is only committed by the settled message below.
2750
+ publishCardSize(w, h, { phase:"prepare", motionId });
2740
2751
  // The scroller's width is FIXED (containment: the height spring must not
2741
2752
  // reflow the rows every frame) — so it must be re-fixed to each new
2742
2753
  // TARGET, once per morph. Content lays out at the destination size
@@ -2744,7 +2755,13 @@
2744
2755
  // its content in a 344px column and the new width is dead space. The
2745
2756
  // folded pill hides .scroll entirely, so never shrink it below EXPANDED.
2746
2757
  scrollEl.style.width = Math.max(w, EXPANDED.w) + "px";
2747
- if (REDUCED) { W.v = w; H.v = h; cardEl.style.width = w + "px"; cardEl.style.height = h + "px"; publishCardSize(w, h); return; }
2758
+ if (REDUCED) {
2759
+ W.v = w; H.v = h;
2760
+ cardEl.style.width = w + "px";
2761
+ cardEl.style.height = h + "px";
2762
+ publishCardSize(w, h, { phase:"settled", motionId });
2763
+ return;
2764
+ }
2748
2765
  // will-change on width/height buys nothing (they are layout, not composited)
2749
2766
  // and costs a layer promotion; containment on .scroll is what makes this cheap.
2750
2767
  if (!raf) { lastT = performance.now(); raf = requestAnimationFrame(frame); }
@@ -2970,7 +2987,7 @@
2970
2987
  if (raf) { cancelAnimationFrame(raf); raf = null; cardEl.style.willChange = ""; }
2971
2988
  cardEl.style.width = w + "px";
2972
2989
  cardEl.style.height = h + "px";
2973
- publishCardSize(w, h);
2990
+ publishCardSize(w, h, { phase:"settled", motionId:++cardMotionId });
2974
2991
  }
2975
2992
  function resetOffstage() {
2976
2993
  ghost = false;
@@ -12021,7 +12038,7 @@
12021
12038
  // authoritative across reloads and future layout changes. Publish the actual
12022
12039
  // initial target before readiness so the native hit rect can never retain a
12023
12040
  // larger stale/default size until the first fold-expand transition.
12024
- publishCardSize(W.t, H.t);
12041
+ publishCardSize(W.t, H.t, { phase:"settled", motionId:cardMotionId });
12025
12042
  // LAST signal on purpose: every listener above is registered and the native
12026
12043
  // hit rect is synchronized, so main may now deliver queued arrivals without
12027
12044
  // a single one being dropped (FM-1).
package/overlay/main.cjs CHANGED
@@ -4033,7 +4033,6 @@ function pollHosts({ probeFrontmost = true } = {}) {
4033
4033
  const CARD_INITIAL = { w: 344, h: 524 }; // EXPANDED in inbox.html; renderer publishes its live size before announcing readiness
4034
4034
  const CARD_MAX = { w: 720, h: 800 }; // READER in inbox.html (the pill expanded into the page; PEEK is 400px wide)
4035
4035
  let cardSize = { w: CARD_INITIAL.w, h: CARD_INITIAL.h };
4036
- let overlayFitTimer = null;
4037
4036
 
4038
4037
  function fitOverlayWindowToCard({ settle = false } = {}) {
4039
4038
  if (!win || win.isDestroyed()) return;
@@ -4043,25 +4042,15 @@ function fitOverlayWindowToCard({ settle = false } = {}) {
4043
4042
  // morphs in place and, importantly, never snaps a user-dragged pill home.
4044
4043
  const target = resizedOverlayBounds(current, cardSize, { maximum: CARD_MAX });
4045
4044
  if (target.x === current.x && target.y === current.y && target.width === current.width && target.height === current.height) {
4046
- if (overlayFitTimer) clearTimeout(overlayFitTimer);
4047
- overlayFitTimer = null;
4048
4045
  return;
4049
4046
  }
4050
4047
  const growing = target.width > current.width || target.height > current.height;
4051
- if (growing || settle) {
4052
- if (overlayFitTimer) clearTimeout(overlayFitTimer);
4053
- overlayFitTimer = null;
4054
- try { win.setBounds(target, false); } catch {}
4055
- return;
4056
- }
4057
- // Let the card's closing animation finish before shrinking the native window;
4058
- // growing happens first so content is never clipped.
4059
- if (overlayFitTimer) clearTimeout(overlayFitTimer);
4060
- overlayFitTimer = setTimeout(() => {
4061
- overlayFitTimer = null;
4062
- fitOverlayWindowToCard({ settle: true });
4063
- }, 420);
4064
- overlayFitTimer.unref?.();
4048
+ // Growth happens before the renderer paints its larger destination. Shrink
4049
+ // waits for an explicit renderer-settled receipt: no guessed timeout, no
4050
+ // competing AppKit resize during the spring, and no stale collapse after a
4051
+ // rapid retarget.
4052
+ if (!growing && !settle) return;
4053
+ try { win.setBounds(target, false); } catch {}
4065
4054
  }
4066
4055
 
4067
4056
  function createWindow() {
@@ -7374,10 +7363,23 @@ ipcMain.on("relay:stall", (_e, info) => {
7374
7363
  }, 500).unref?.();
7375
7364
  }
7376
7365
 
7377
- ipcMain.on("relay:cardSize", (_e, w, h) => {
7366
+ let latestCardMotionId = 0;
7367
+ let latestCardMotionSessionId = "";
7368
+ ipcMain.on("relay:cardSize", (event, w, h, motion = {}) => {
7369
+ if (!win || win.isDestroyed() || event.sender !== win.webContents) return;
7378
7370
  if (!Number.isFinite(w) || !Number.isFinite(h)) return;
7371
+ const motionSessionId = typeof motion.motionSessionId === "string"
7372
+ ? motion.motionSessionId.slice(0, 64)
7373
+ : "";
7374
+ const motionId = Number.isSafeInteger(motion.motionId) ? motion.motionId : 0;
7375
+ if (motionSessionId !== latestCardMotionSessionId) {
7376
+ latestCardMotionSessionId = motionSessionId;
7377
+ latestCardMotionId = 0;
7378
+ }
7379
+ if (motionId < latestCardMotionId) return;
7380
+ latestCardMotionId = motionId;
7379
7381
  cardSize = { w, h };
7380
- fitOverlayWindowToCard();
7382
+ fitOverlayWindowToCard({ settle: motion.phase === "settled" });
7381
7383
  });
7382
7384
  ipcMain.handle("relay:prepareCardSize", (event, w, h) => {
7383
7385
  if (!win || win.isDestroyed() || event.sender !== win.webContents) return { ok:false };
@@ -177,8 +177,15 @@ contextBridge.exposeInMainWorld("relay", {
177
177
 
178
178
  // window plumbing
179
179
  engage: () => ipcRenderer.send("relay:engage"),
180
- // The native window tracks the visible card's dimensions.
181
- cardSize: (w, h) => ipcRenderer.send("relay:cardSize", w, h),
180
+ // The native window tracks the visible card's dimensions. `prepare` reserves
181
+ // growth before paint; `settled` commits shrink after the spring completes.
182
+ cardSize: (w, h, motion = {}) => ipcRenderer.send("relay:cardSize", w, h, {
183
+ phase: motion.phase === "settled" ? "settled" : "prepare",
184
+ motionId: Number.isSafeInteger(motion.motionId) ? motion.motionId : 0,
185
+ motionSessionId: typeof motion.motionSessionId === "string"
186
+ ? motion.motionSessionId.slice(0, 64)
187
+ : "",
188
+ }),
182
189
  // Grow before revealing a larger reader so content is never clipped.
183
190
  prepareCardSize: (w, h) => ipcRenderer.invoke("relay:prepareCardSize", w, h),
184
191
  setTheme: (t) => ipcRenderer.send("relay:theme", t), // preview window wears the same sheet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.366",
3
+ "version": "0.1.367",
4
4
  "description": "Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.",
5
5
  "homepage": "https://sendrelays.com/get-started",
6
6
  "repository": {