relay-companion 0.1.89 → 0.1.90

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.
@@ -703,7 +703,8 @@
703
703
  the reused .open-actions light-built panel — never from borders. */
704
704
  .sv-head { display:flex; align-items:center; justify-content:space-between; padding:10px 16px 8px; }
705
705
  .sv-title { font-family:var(--serif); font-size:16px; font-weight:500; color:var(--ink); }
706
- .sv-account { display:flex; align-items:center; gap:12px; padding:4px 16px 0; }
706
+ /* First element in the pane now (no header above), so it owns the top inset. */
707
+ .sv-account { display:flex; align-items:center; gap:12px; padding:16px 16px 0; }
707
708
  .sv-avatar {
708
709
  flex:0 0 auto; width:38px; height:38px; border-radius:50%;
709
710
  background:var(--accent-soft); color:var(--accent);
@@ -1561,6 +1562,10 @@
1561
1562
  // The relay row whose open-actions card is expanded. Like openingIds, this
1562
1563
  // survives the innerHTML rebuilds renderRelays performs on every push.
1563
1564
  let expandedRelayId = null;
1565
+ // A conversation expands its actions IN PLACE, exactly like a single relay.
1566
+ // Taking over the whole pane (the old thread-detail push) lost the user's
1567
+ // place in the list for what is really a three-item menu.
1568
+ let expandedThreadId = null;
1564
1569
  function openingTargets(id) {
1565
1570
  return [...document.querySelectorAll("[data-opening-id]")].filter((e) => e.getAttribute("data-opening-id") === id);
1566
1571
  }
@@ -1676,8 +1681,13 @@
1676
1681
  if (t.msgs.length === 1 && inbound.length === 1 && byId.has(inbound[0].id)) {
1677
1682
  return relayRowHtml(byId.get(inbound[0].id), seen);
1678
1683
  }
1684
+ const tExpanded = t.threadId === expandedThreadId;
1685
+ const newestInbound = [...t.msgs].reverse().find((m) => m.direction === "in") || t.msgs[t.msgs.length - 1];
1686
+ const tMenu = tExpanded && newestInbound
1687
+ ? openActionsHtml(newestInbound.id, { thread: true, threadId: t.threadId, messageCount: t.msgs.length })
1688
+ : "";
1679
1689
  return `
1680
- <div class="th-item${t.unreadCount ? " unread" : ""} deletable" role="button" tabindex="0" data-thread="${esc(t.threadId)}" data-party="${esc(t.party)}">
1690
+ <div class="th-item${t.unreadCount ? " unread" : ""}${tExpanded ? " expanded" : ""} deletable" role="button" tabindex="0" data-thread="${esc(t.threadId)}" data-party="${esc(t.party)}">
1681
1691
  ${threadDeleteButton(t)}
1682
1692
  <span class="th-top">
1683
1693
  ${t.unreadCount ? '<span class="th-unread"></span>' : ""}
@@ -1686,13 +1696,20 @@
1686
1696
  <span class="th-time">${esc(timeAgo(t.latest.at))}</span>
1687
1697
  </span>
1688
1698
  <span class="th-title">${esc(t.latest.title)}</span>
1699
+ ${tMenu}
1689
1700
  </div>`;
1690
1701
  }).join("");
1691
1702
  prevRelayIds = new Set(allRows.map((r) => r.id));
1692
1703
  wireRelayRows();
1693
1704
  for (const el of relaysListEl.querySelectorAll(".th-item")) {
1694
- el.addEventListener("click", () => openThreadDetail(el.getAttribute("data-thread"), el.getAttribute("data-party") || ""));
1705
+ el.addEventListener("click", () => {
1706
+ const id = el.getAttribute("data-thread");
1707
+ expandedThreadId = expandedThreadId === id ? null : id;
1708
+ expandedRelayId = null; // only one menu open at a time
1709
+ renderRelays();
1710
+ });
1695
1711
  }
1712
+ wireInlineThreadActions();
1696
1713
  }
1697
1714
 
1698
1715
  // Grow the window as the user scrolls toward the end. Cheap: each extension
@@ -1711,7 +1728,7 @@
1711
1728
  // Preview plus the two host-open actions, as one small text-first menu.
1712
1729
  // Preview is the primary, non-navigation path; the other actions hand the
1713
1730
  // relay to a current or fresh host session.
1714
- function openActionsHtml(id, { thread, previewable = true } = {}) {
1731
+ function openActionsHtml(id, { thread, previewable = true, threadId = "", messageCount = 0 } = {}) {
1715
1732
  const preview = thread ? `data-thread-preview="${esc(id)}"` : `data-preview="${esc(id)}"`;
1716
1733
  const cur = thread ? `data-thread-open-current="${esc(id)}"` : `data-open-current="${esc(id)}"`;
1717
1734
  const fresh = thread ? `data-thread-open-fresh="${esc(id)}"` : `data-open-fresh="${esc(id)}"`;
@@ -1724,9 +1741,37 @@
1724
1741
  <button class="oa-item${previewable ? "" : " oa-primary"}" type="button" role="menuitem" ${cur}>${thread ? "Open Thread in Current Chat" : "Open in Current Chat"}</button>
1725
1742
  <div class="oa-sep" role="separator"></div>
1726
1743
  <button class="oa-item" type="button" role="menuitem" ${fresh}>${thread ? "Open Thread in New Chat" : "Open in New Chat"}</button>
1744
+ ${threadId ? `<div class="oa-sep" role="separator"></div>
1745
+ <button class="oa-item" type="button" role="menuitem" data-thread-history="${esc(threadId)}">Read the Conversation${messageCount ? ` (${messageCount})` : ""}</button>` : ""}
1727
1746
  </div>`;
1728
1747
  }
1729
1748
 
1749
+
1750
+ // The inline conversation menu reuses the thread actions; scoped to the list so
1751
+ // the detail view's own copy keeps working unchanged.
1752
+ function wireInlineThreadActions() {
1753
+ for (const btn of relaysListEl.querySelectorAll("[data-thread-preview]")) {
1754
+ const id = btn.getAttribute("data-thread-preview");
1755
+ btn.addEventListener("click", () => {
1756
+ expandedThreadId = null;
1757
+ window.relay.preview(id);
1758
+ renderRelays();
1759
+ });
1760
+ }
1761
+ for (const btn of relaysListEl.querySelectorAll("[data-thread-open-current]")) {
1762
+ btn.addEventListener("click", () => openRelayFromUI(btn.getAttribute("data-thread-open-current"), "relay", "current"));
1763
+ }
1764
+ for (const btn of relaysListEl.querySelectorAll("[data-thread-open-fresh]")) {
1765
+ btn.addEventListener("click", () => openRelayFromUI(btn.getAttribute("data-thread-open-fresh"), "relay", "fresh"));
1766
+ }
1767
+ for (const btn of relaysListEl.querySelectorAll("[data-thread-history]")) {
1768
+ btn.addEventListener("click", () => {
1769
+ const el = btn.closest(".th-item");
1770
+ openThreadDetail(btn.getAttribute("data-thread-history"), el ? el.getAttribute("data-party") || "" : "");
1771
+ });
1772
+ }
1773
+ }
1774
+
1730
1775
  // Conversations get the same trash affordance as single relays — the unified
1731
1776
  // list made that inconsistent (a 1-message conversation renders as a relay row
1732
1777
  // WITH delete; a 2-message one rendered as a conversation row without). One
@@ -3065,7 +3110,6 @@
3065
3110
  if (info.paired) {
3066
3111
  const name = info.name || info.email || "Relay account";
3067
3112
  html += `
3068
- <div class="sv-head"><span class="sv-title">Your account.</span></div>
3069
3113
  <div class="sv-account">
3070
3114
  <span class="sv-avatar">${esc(cvInitials(info.name, info.email))}</span>
3071
3115
  <span class="sv-id">
package/overlay/main.cjs CHANGED
@@ -2330,12 +2330,19 @@ function anchorTopRight() {
2330
2330
  const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) || screen.getPrimaryDisplay();
2331
2331
  const wa = display.workArea;
2332
2332
  const anchor = { x: wa.x + wa.width - WIN.width - MARGIN, y: wa.y + MARGIN, width: WIN.width, height: WIN.height };
2333
- if (process.env.RELAY_OVERLAY_TEST === "1") {
2334
- // Sandboxed harness runs must never materialize under the user's parked
2335
- // cursor: a pointer resting on the card holds notifications open BY DESIGN
2336
- // (macOS banners do the same), which would wedge every dwell-based scenario
2337
- // when the mouse happens to sit in the top-right corner. Flip the sandbox
2338
- // window to the opposite edge instead. Production placement is unchanged.
2333
+ if (process.env.RELAY_OVERLAY_TEST === "1" || process.env.RELAY_OVERLAY_PERF === "1") {
2334
+ // A harness run must not take over the developer's screen. Park sandbox
2335
+ // windows just off the bottom-right of the work area: still a REAL composited
2336
+ // window on the same display — so paint timing, dwell timers and hit tests
2337
+ // measure exactly what production does — but out of sight while the suite
2338
+ // runs, and never under the user's parked cursor (a pointer resting on the
2339
+ // card holds notifications open BY DESIGN, which wedged dwell scenarios).
2340
+ // RELAY_OVERLAY_TEST_ONSCREEN=1 puts it back on screen for eyeball debugging.
2341
+ if (process.env.RELAY_OVERLAY_TEST_ONSCREEN !== "1") {
2342
+ anchor.x = wa.x + wa.width - 8;
2343
+ anchor.y = wa.y + wa.height - 8;
2344
+ return anchor;
2345
+ }
2339
2346
  try {
2340
2347
  const p = screen.getCursorScreenPoint();
2341
2348
  const overlaps =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "description": "Relay companion for ordinary messages, with dormant coordination features available only by explicit opt-in.",
5
5
  "type": "module",
6
6
  "bin": {