relay-companion 0.1.365 → 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.365",
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.365",
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.365",
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": {
package/src/mcp.js CHANGED
@@ -233,7 +233,7 @@ export const TOOLS = [
233
233
  name: "relay_send",
234
234
  _meta: ALWAYS_LOAD_META,
235
235
  description:
236
- "Only send a Relay when the user asks you to send (or relay) something to someone. Send ordinary Relay correspondence or a Task. Default to Relay when the user explicitly says Relay or asks to send, share, tell, ask, message, or hand something to a named person or saved group without specifying a medium; an explicit other medium overrides. When the recipient is this human themselves ('me', 'myself', or their own account), call relay_send directly with recipient.self=true; never search contacts or mint a share link for a self-Relay. Resolve every other recipient with relay_contacts_search or relay_groups_list. Without a confident exact match, mint a link with relay_share_link and hand this human the url to paste; never ask for an email address and never fall back to another medium. CLASSIFY THE OUTCOME, NOT THE SENTENCE'S ADDRESSEE: kind='message' seeks the person's attention, opinion, decision, or reply; kind='task' asks their agent to perform external work. A technical subject or non-empty forAgent does not itself make a Task. Compose the complete forAgent document first, then derive forHuman as the concise message in the sender's voice. Use the fewest words that faithfully preserve the message, not the most allowed: 1-3 normally sized sentences and 45 words or fewer. 60 words is only Relay's mandatory-review threshold, never a target, budget, or default; it stops an over-threshold first attempt for review. Message history teaches voice and relationship register, not a default word count. Under-sending to the recipient's agent is worse than over-sending. Relay-owned Task Runs attach their provider's final answer automatically; do not call relay_send merely to report completion. Addressing a person, group, or chat never implies a reply to its latest message. Set replyToRelayId only when the human explicitly wants to quote or answer that exact Relay. For a Granular digital employee, use the exact matching workspace-labelled contactId.",
236
+ "Only send a Relay when the user asks you to send (or relay) something to someone. When writing a Relay, make normal choices about wording and presentation without asking the user. Ask a clarifying question before sending only when both are true: you are unsure about a detail, and resolving it one way or another could substantially change what the Relay says or commits the user to. Most Relay requests do not require clarification. Send ordinary Relay correspondence or a Task. Default to Relay when the user explicitly says Relay or asks to send something to someone without specifying a medium; another named medium overrides. For a self-Relay, call relay_send with recipient.self=true; never search contacts or mint a link. Resolve other recipients with relay_contacts_search or relay_groups_list. Without a confident match, mint a link with relay_share_link; never ask for an email or switch media. CLASSIFY THE OUTCOME, NOT THE SENTENCE'S ADDRESSEE: kind='message' seeks the person's attention, opinion, decision, or reply; kind='task' asks their agent to perform external work. Compose the complete forAgent document first, then derive forHuman as the concise message in the sender's voice. Use the fewest words that faithfully preserve the message, not the most allowed: 1-3 normally sized sentences and 45 words or fewer. 60 words is only Relay's mandatory-review threshold, never a target, budget, or default; it stops an over-threshold first attempt for review. Message history teaches voice and relationship register, not a default word count. Under-sending to the recipient's agent is worse than over-sending. Relay-owned Task Runs attach their provider's final answer automatically; do not call relay_send merely to report completion. Addressing a person, group, or chat never implies a reply to its latest message. Set replyToRelayId only when the human explicitly wants to quote or answer that exact Relay. For a Granular digital employee, use the exact matching workspace-labelled contactId.",
237
237
  inputSchema: {
238
238
  type: "object",
239
239
  properties: {
@@ -513,7 +513,7 @@ export const TOOLS = [
513
513
  {
514
514
  name: "relay_chat_send",
515
515
  description:
516
- "Only send a Relay when the user asks you to send (or relay) something to someone. Send an ordinary text message into an existing Relay chat. chatId addresses the room; it does not imply a reply to the newest message. Set replyToRelayId only when the human explicitly selected or named a specific message to quote. Supports the same local-file attachment forms as relay_send. This always sends kind='message'; use relay_send for a Task or a separate forAgent document.",
516
+ "Only send a Relay when the user asks you to send (or relay) something to someone. When writing a Relay, make normal choices about wording and presentation without asking the user. Ask a clarifying question before sending only when both are true: you are unsure about a detail, and resolving it one way or another could substantially change what the Relay says or commits the user to. Most Relay requests do not require clarification. Send an ordinary text message into an existing Relay chat. chatId addresses the room; it does not imply a reply to the newest message. Set replyToRelayId only when the human explicitly selected or named a specific message to quote. Supports the same local-file attachment forms as relay_send. This always sends kind='message'; use relay_send for a Task or a separate forAgent document.",
517
517
  inputSchema: CHAT_SEND_INPUT_SCHEMA,
518
518
  },
519
519
  {