relay-companion 0.1.74 → 0.1.76

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.
@@ -578,8 +578,10 @@
578
578
  .cv-hint:empty { display:none; }
579
579
 
580
580
  /* ---- Threads tab: thread rows + one thread's message history ---- */
581
+ /* A div, not a button: it hosts the delete button, and nesting buttons is
582
+ invalid HTML (the parser hoists the inner one and breaks the row). */
581
583
  .th-item {
582
- display:block; width:100%; text-align:left; appearance:none; border:0; cursor:pointer;
584
+ position:relative; display:block; width:100%; text-align:left; appearance:none; border:0; cursor:pointer;
583
585
  padding:11px 16px; border-bottom:1px solid var(--hair); background:transparent;
584
586
  font-family:var(--sans); transition:background-color .14s var(--settle);
585
587
  }
@@ -770,9 +772,9 @@
770
772
  </div>
771
773
  <div id="thDetail" class="gone">
772
774
  <div class="th-detail-head">
773
- <button class="th-back" type="button" id="thBack" aria-label="Back to threads">
775
+ <button class="th-back" type="button" id="thBack" aria-label="Back to relays">
774
776
  <svg width="7" height="12" viewBox="0 0 8 14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M7 1L1 7l6 6"/></svg>
775
- Threads
777
+ Relays
776
778
  </button>
777
779
  <span class="th-detail-name" id="thDetailName"></span>
778
780
  </div>
@@ -1582,7 +1584,8 @@
1582
1584
  }
1583
1585
  const sentOnly = inbound.length === 0;
1584
1586
  return `
1585
- <button class="th-item${t.unreadCount ? " unread" : ""}${sentOnly ? " th-sent-only" : ""}" type="button" data-thread="${esc(t.threadId)}">
1587
+ <div class="th-item${t.unreadCount ? " unread" : ""}${sentOnly ? " th-sent-only" : ""} deletable" role="button" tabindex="0" data-thread="${esc(t.threadId)}" data-party="${esc(t.party)}">
1588
+ ${threadDeleteButton(t)}
1586
1589
  <span class="th-top">
1587
1590
  ${t.unreadCount ? '<span class="th-unread"></span>' : ""}
1588
1591
  <span class="th-party">${sentOnly ? "To " : ""}${esc(t.party)}</span>
@@ -1590,12 +1593,12 @@
1590
1593
  <span class="th-time">${esc(timeAgo(t.latest.at))}</span>
1591
1594
  </span>
1592
1595
  <span class="th-title">${esc(t.latest.title)}</span>
1593
- </button>`;
1596
+ </div>`;
1594
1597
  }).join("");
1595
1598
  prevRelayIds = new Set(allRows.map((r) => r.id));
1596
1599
  wireRelayRows();
1597
1600
  for (const el of relaysListEl.querySelectorAll(".th-item")) {
1598
- el.addEventListener("click", () => openThreadDetail(el.getAttribute("data-thread")));
1601
+ el.addEventListener("click", () => openThreadDetail(el.getAttribute("data-thread"), el.getAttribute("data-party") || ""));
1599
1602
  }
1600
1603
  }
1601
1604
 
@@ -1613,6 +1616,17 @@
1613
1616
  </div>`;
1614
1617
  }
1615
1618
 
1619
+ // Conversations get the same trash affordance as single relays — the unified
1620
+ // list made that inconsistent (a 1-message conversation renders as a relay row
1621
+ // WITH delete; a 2-message one rendered as a conversation row without). One
1622
+ // click removes every inbound relay in the conversation; outbound copies are
1623
+ // the recipient's to delete, so a sent-only conversation has nothing to trash.
1624
+ function threadDeleteButton(t) {
1625
+ const ids = (t.msgs || []).filter((m) => m.direction === "in").map((m) => m.id).filter(Boolean);
1626
+ if (!ids.length) return "";
1627
+ return `<button class="row-delete" type="button" data-delete-thread="${esc(ids.join(","))}" data-stop="1" aria-label="Move this conversation to Recently Deleted" title="Move this conversation to Recently Deleted"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5"/></svg></button>`;
1628
+ }
1629
+
1616
1630
  // The classic relay row (used flat while peeking, and for single-message
1617
1631
  // conversations in the unified list). Row click toggles the open-actions
1618
1632
  // card; the buttons carry the actual opens.
@@ -1744,6 +1758,19 @@
1744
1758
  ta.addEventListener("input", () => { ta.style.height = "auto"; ta.style.height = Math.min(84, ta.scrollHeight) + "px"; });
1745
1759
  ta.addEventListener("mousedown", () => setInteractive(true));
1746
1760
  }
1761
+ for (const btn of relaysListEl.querySelectorAll("[data-delete-thread]")) {
1762
+ const ids = (btn.getAttribute("data-delete-thread") || "").split(",").filter(Boolean);
1763
+ btn.addEventListener("click", async (event) => {
1764
+ event.stopPropagation();
1765
+ if (btn.disabled || !ids.length) return;
1766
+ btn.disabled = true;
1767
+ try {
1768
+ for (const id of ids) await window.relay.deleteRelay(id);
1769
+ } catch {
1770
+ btn.disabled = false;
1771
+ }
1772
+ });
1773
+ }
1747
1774
  for (const btn of relaysListEl.querySelectorAll("[data-delete]")) {
1748
1775
  const id = btn.getAttribute("data-delete");
1749
1776
  btn.addEventListener("click", async (event) => {
@@ -2530,6 +2557,7 @@
2530
2557
  // relays plus the live Sent list. Local data only — the pill shows the
2531
2558
  // user's own exchanges; the API's /v1/threads serves web and agents.
2532
2559
  let threadDetailId = null; // threadId while a thread's history is open
2560
+ let threadDetailPartyHint = ""; // who it's with, for the header while messages load
2533
2561
  function threadMessages() {
2534
2562
  const msgs = [];
2535
2563
  for (const r of payload.relays || []) {
@@ -2602,8 +2630,14 @@
2602
2630
  function renderThreadDetail() {
2603
2631
  const thread = buildThreads().find((t) => t.threadId === threadDetailId);
2604
2632
  if (!thread) {
2605
- threadDetailId = null;
2606
- return renderThreads();
2633
+ // A conversation whose messages live on the SENT side renders empty until
2634
+ // the sent cache loads — and since 0.1.73 that refresh is lazy (30s when
2635
+ // idle). Ask for it now and show a loading line rather than a blank pane;
2636
+ // the next payload re-renders this view with the messages.
2637
+ thDetailNameEl.textContent = threadDetailPartyHint || "Conversation";
2638
+ thHistoryEl.innerHTML = '<div class="cv-empty">Loading conversation…</div>';
2639
+ if (window.relay.refreshSent) window.relay.refreshSent().catch(() => {});
2640
+ return;
2607
2641
  }
2608
2642
  thDetailNameEl.textContent = thread.party;
2609
2643
  // Thread-level opens act on the newest inbound message (falling back to the
@@ -2638,8 +2672,9 @@
2638
2672
  applyView();
2639
2673
  renderAll();
2640
2674
  });
2641
- function openThreadDetail(threadId) {
2675
+ function openThreadDetail(threadId, partyHint = "") {
2642
2676
  threadDetailId = threadId;
2677
+ threadDetailPartyHint = partyHint;
2643
2678
  activeView = "threads";
2644
2679
  syncTabs();
2645
2680
  applyView();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
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": {
@@ -623,6 +623,13 @@ export function spawnDetachedUpdate({
623
623
  ` echo "[relay-update] $(date) electron runtime missing; running electron install.js" >> ${LOG} 2>&1`,
624
624
  ` "$NODE" "$ELECTRON_DIR/install.js" >> ${LOG} 2>&1 || true`,
625
625
  ` fi`,
626
+ // A half-extracted dist (binary present, Frameworks missing) SIGABRTs at
627
+ // launch and passed the old executable-only check. Force a clean re-extract.
628
+ ` ELECTRON_FRAMEWORKS="$ELECTRON_DIR/dist/Electron.app/Contents/Frameworks/Electron Framework.framework"`,
629
+ ` if [ -x "$ELECTRON" ] && [ ! -d "$ELECTRON_FRAMEWORKS" ] && [ -f "$ELECTRON_DIR/install.js" ]; then`,
630
+ ` echo "[relay-update] $(date) electron dist incomplete (no Frameworks); re-extracting" >> ${LOG} 2>&1`,
631
+ ` rm -rf "$ELECTRON_DIR/dist" 2>/dev/null; "$NODE" "$ELECTRON_DIR/install.js" >> ${LOG} 2>&1 || true`,
632
+ ` fi`,
626
633
  // Verify the package at the exact launchd-owned root reached the exact discovered
627
634
  // version. A mismatched prefix or stale npm response must never trigger restart.
628
635
  ` INSTALLED_VERSION="$("$NODE" -e 'process.stdout.write(require(process.argv[1]).version)' "$PKG/package.json" 2>/dev/null)"`,
@@ -631,7 +638,7 @@ export function spawnDetachedUpdate({
631
638
  ? ` VERSION_OK=0; if [ "$INSTALLED_VERSION" = ${q(target)} ]; then VERSION_OK=1; else echo "[relay-update] $(date) verify FAILED: installed $INSTALLED_VERSION != exact target ${target}" >> ${LOG} 2>&1; fi`
632
639
  : ` VERSION_OK=1`,
633
640
  ` DESKTOP_OK=0`,
634
- ` if [ "$VERSION_OK" = 1 ] && [ -x "$ELECTRON" ] && [ -f "$PKG/bin/relay.js" ] && grep -q '"version"' "$PKG/package.json" 2>/dev/null; then`,
641
+ ` if [ "$VERSION_OK" = 1 ] && [ -x "$ELECTRON" ] && [ -d "$ELECTRON_FRAMEWORKS" ] && [ -f "$PKG/bin/relay.js" ] && grep -q '"version"' "$PKG/package.json" 2>/dev/null; then`,
635
642
  // Rebuild version-coupled app/plist paths from the NEW package before restart.
636
643
  // --no-restart keeps sequencing here: pill first, daemon (our parent) last.
637
644
  ` if "$NODE" "$PKG/bin/relay.js" repair-desktop --no-restart${repairModeArgs ? ` ${repairModeArgs}` : ""} >> ${LOG} 2>&1; then DESKTOP_OK=1; else echo "[relay-update] $(date) desktop repair FAILED" >> ${LOG} 2>&1; fi`,
package/src/client.js CHANGED
@@ -1,4 +1,16 @@
1
1
  import { apiUrl, deviceToken } from "./config.js";
2
+ import { createRequire } from "node:module";
3
+
4
+ // Reported on every device-authenticated call so the server always knows which
5
+ // companion version each device runs — support and rollout questions get
6
+ // answered from data, never from screenshots.
7
+ const COMPANION_VERSION = (() => {
8
+ try {
9
+ return String(createRequire(import.meta.url)("../package.json").version || "");
10
+ } catch {
11
+ return "";
12
+ }
13
+ })();
2
14
 
3
15
  /** Thin authenticated client for relay-api, used by the CLI, MCP server, and daemon. */
4
16
  export class RelayClient {
@@ -9,6 +21,7 @@ export class RelayClient {
9
21
 
10
22
  async #req(method, path, body, { auth = true } = {}) {
11
23
  const headers = { "Content-Type": "application/json" };
24
+ if (COMPANION_VERSION) headers["x-relay-version"] = COMPANION_VERSION;
12
25
  if (auth && this.token) headers.Authorization = `Bearer ${this.token}`;
13
26
  const res = await fetch(`${this.url}${path}`, {
14
27
  method,