relay-companion 0.1.90 → 0.1.91

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.
@@ -251,11 +251,11 @@
251
251
 
252
252
  /* subject: single-line serif, right-edge fade mask */
253
253
  .rk-subject {
254
- font-family:var(--serif); font-size:15px; line-height:1.3; color:var(--muted); font-weight:400;
254
+ font-family:var(--serif); font-size:15px; line-height:1.32; color:var(--muted); font-weight:400;
255
255
  margin-top:5px;
256
256
  /* Two full lines with an ellipsis: titles must be scannable when choosing
257
257
  which relay to open — the old right-edge fade cut them after a few words. */
258
- overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
258
+ overflow:hidden; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical;
259
259
  overflow-wrap:anywhere;
260
260
  transition:color .28s var(--settle);
261
261
  }
@@ -743,6 +743,8 @@
743
743
  .sv-err { margin-top:8px; padding:0 16px; }
744
744
  .sv-note { padding:10px 16px 0; font-size:11.5px; color:#1a7f37; line-height:1.4; }
745
745
  .sv-version { padding:20px 16px 12px; font-family:var(--mono); font-size:10.5px; color:var(--muted-3); }
746
+ .sv-update { display:flex; flex-direction:column; gap:8px; align-items:flex-start; }
747
+ .sv-update-btn { width:auto; padding:7px 12px; border-radius:8px; font-family:var(--sans); font-size:12px; }
746
748
  /* signed-out call-to-action on the Relays empty state — quiet filled pill (th-count language) */
747
749
  .sv-signin-banner {
748
750
  appearance:none; border:0; cursor:pointer; margin-top:2px;
@@ -1697,6 +1699,7 @@
1697
1699
  </span>
1698
1700
  <span class="th-title">${esc(t.latest.title)}</span>
1699
1701
  ${tMenu}
1702
+ ${newestInbound ? `<div class="row-err${rowNotes.get(newestInbound.id)?.cls === "ok" ? " ok" : ""}" data-err="${esc(newestInbound.id)}" data-stop="1">${esc(rowNotes.get(newestInbound.id)?.text || "")}</div>` : ""}
1700
1703
  </div>`;
1701
1704
  }).join("");
1702
1705
  prevRelayIds = new Set(allRows.map((r) => r.id));
@@ -1792,11 +1795,11 @@
1792
1795
  const subject = relaySubject(r);
1793
1796
  const sender = relaySender(r);
1794
1797
  const actions = relayActionsHtml(r);
1795
- // result / task_completed / plain message carry a body preview beneath the subject.
1796
- // (human_question's body IS the subject, so no duplicate preview there.)
1797
- const body = r.bodyMarkdown || r.body || "";
1798
- const preview = kind === "human_question" ? "" : bodyPreview(body, subject, 110);
1799
- const previewHtml = preview ? `<div class="rk-preview">${esc(preview)}</div>` : "";
1798
+ // Cards show the TITLE ONLY. The old truncated body preview leaked the
1799
+ // agent-oriented detail layer — often unreadable out of context — and cost a
1800
+ // third of the row to say little. Titles now carry what/why/ask (see
1801
+ // relay_send's title guidance), so the detail waits for Preview or an open.
1802
+ const previewHtml = "";
1800
1803
  const canDelete = kind === "plain_relay" || kind === "human_question" || kind === "task_completed" || kind === "task_cancelled" || kind === "task_failed";
1801
1804
  const deleteButton = canDelete ? `<button class="row-delete" type="button" data-delete="${esc(r.id)}" data-stop="1" aria-label="Move to Recently Deleted" title="Move 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>` : "";
1802
1805
  const expanded = r.id === expandedRelayId;
@@ -3033,7 +3036,15 @@
3033
3036
  const threadActions = threadOpenTarget
3034
3037
  ? openActionsHtml(threadOpenTarget.id, { thread: true, previewable: Boolean(newestInbound) })
3035
3038
  : "";
3036
- thHistoryEl.innerHTML = threadActions + thread.msgs.map((m) => `
3039
+ // Open-status notes ("Handed to your current chat…" / "✓ Delivered" /
3040
+ // open errors) must render HERE too: users who live in the thread view
3041
+ // click these buttons and previously got zero feedback — a working
3042
+ // hand-off was indistinguishable from a dead one (Sven, 2026-08-05,
3043
+ // twice). Rendered from rowNotes so every payload-push rebuild keeps it.
3044
+ const threadNote = threadOpenTarget
3045
+ ? `<div class="row-err${rowNotes.get(threadOpenTarget.id)?.cls === "ok" ? " ok" : ""}" data-err="${esc(threadOpenTarget.id)}" data-stop="1">${esc(rowNotes.get(threadOpenTarget.id)?.text || "")}</div>`
3046
+ : "";
3047
+ thHistoryEl.innerHTML = threadActions + threadNote + thread.msgs.map((m) => `
3037
3048
  <button class="th-msg${m.unread ? " unread" : ""}" type="button" data-msg="${esc(m.id)}" data-dir="${m.direction}">
3038
3049
  <span class="th-msg-top">
3039
3050
  <span class="th-dir${m.direction === "out" ? " out" : ""}">${m.direction === "out" ? "←" : "→"}</span>
@@ -3134,11 +3145,28 @@
3134
3145
  if (svPairOpen) html += svPairRowHtml();
3135
3146
  if (svNote) html += `<div class="sv-note">${esc(svNote)}</div>`;
3136
3147
  html += `<div class="row-err sv-err" id="svErr">${esc(svError)}</div>`;
3137
- html += `<div class="sv-version">${esc(info.version ? `Relay ${info.version}` : "Relay")}</div>`;
3148
+ // Version line doubles as the manual update backstop: if the auto-updater
3149
+ // has not refreshed this machine, say so here and make it one click.
3150
+ if (info.updateAvailable) {
3151
+ html += `<div class="sv-version sv-update">
3152
+ <span>Relay ${esc(info.version || "")} · update ${esc(info.updateAvailable)} ready</span>
3153
+ <button class="oa-item oa-primary sv-update-btn" type="button" id="svUpdate" ${info.updating ? "disabled" : ""}>${info.updating ? "Updating…" : "Update and Restart"}</button>
3154
+ </div>`;
3155
+ } else {
3156
+ html += `<div class="sv-version">${esc(info.version ? `Relay ${info.version}` : "Relay")}</div>`;
3157
+ }
3138
3158
  settingsViewEl.innerHTML = html;
3139
3159
  wireSettings();
3140
3160
  }
3141
3161
  function wireSettings() {
3162
+ const upd = document.getElementById("svUpdate");
3163
+ if (upd) {
3164
+ upd.addEventListener("click", async () => {
3165
+ upd.disabled = true;
3166
+ upd.textContent = "Updating…";
3167
+ try { await window.relay.updateNow(); } catch {}
3168
+ });
3169
+ }
3142
3170
  for (const z of settingsViewEl.querySelectorAll('[data-stop="1"]')) {
3143
3171
  z.addEventListener("click", (e) => e.stopPropagation());
3144
3172
  }
package/overlay/main.cjs CHANGED
@@ -451,6 +451,10 @@ function accountInfo() {
451
451
  email: user.email || "",
452
452
  deviceName: String(cfg.deviceName || "").trim() || os.hostname(),
453
453
  version: pillVersion(),
454
+ // Surfaced in the Settings card so a stale machine is visible in the app,
455
+ // not only in the tray menu.
456
+ updateAvailable: availableUpdate || "",
457
+ updating: updateInFlight,
454
458
  setupPath: "/app/setup",
455
459
  };
456
460
  }
@@ -3093,6 +3097,47 @@ function quitRelayCompletely() {
3093
3097
  } catch {}
3094
3098
  }
3095
3099
 
3100
+
3101
+ // ---- update availability (the manual backstop) -----------------------------
3102
+ // The daemon self-updates, but an update can be pending for a while (the
3103
+ // restart cooldown), or fail quietly, and then a machine silently runs old
3104
+ // code — exactly what happened to a Windows user who sat nine versions behind.
3105
+ // The pill therefore checks the registry itself and, when a newer version
3106
+ // exists, surfaces it where a person will actually see it: the tray menu and
3107
+ // the Settings card, each offering a one-click "restart now" that runs the
3108
+ // same installer with manual intent (which bypasses the cooldown).
3109
+ let availableUpdate = null; // version string when newer than what we run
3110
+ let updateInFlight = false;
3111
+ async function checkForUpdate() {
3112
+ try {
3113
+ const mod = await import(pathToFileURL(path.join(__dirname, "..", "src", "auto-update.js")).href);
3114
+ const latest = await mod.fetchLatestVersion({});
3115
+ const current = pillVersion();
3116
+ availableUpdate = latest && mod.isNewerVersion(latest, current) ? latest : null;
3117
+ if (availableUpdate) syncTray();
3118
+ } catch {}
3119
+ }
3120
+ function runUpdateNow() {
3121
+ if (updateInFlight) return;
3122
+ updateInFlight = true;
3123
+ try {
3124
+ const child = spawn(process.execPath, [RELAY_CLI, "update"], {
3125
+ detached: true,
3126
+ stdio: "ignore",
3127
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
3128
+ });
3129
+ child.on("error", (error) => {
3130
+ updateInFlight = false;
3131
+ console.error(`[overlay] ${new Date().toISOString()} manual update failed:`, error && error.message);
3132
+ });
3133
+ child.unref();
3134
+ } catch (error) {
3135
+ updateInFlight = false;
3136
+ console.error(`[overlay] ${new Date().toISOString()} manual update spawn failed:`, error && error.message);
3137
+ }
3138
+ if (win && !win.isDestroyed()) win.webContents.send("updateStarted");
3139
+ }
3140
+
3096
3141
  function createTray() {
3097
3142
  const icon = createTrayIcon();
3098
3143
  if (!icon || icon.isEmpty()) {
@@ -3123,6 +3168,12 @@ function createTray() {
3123
3168
  { label: pillIsOnScreen() ? "Hide Relay" : "Show Relay", click: toggleFromTray },
3124
3169
  ...previewItem,
3125
3170
  { type: "separator" },
3171
+ // Mirrors what good menu-bar apps do: state the version you are on,
3172
+ // and when a newer one is waiting make restarting a single click.
3173
+ ...(availableUpdate
3174
+ ? [{ label: `Update ready (${availableUpdate}) — restart now?`, click: runUpdateNow }]
3175
+ : [{ label: `Relay ${pillVersion()}`, enabled: false }]),
3176
+ { type: "separator" },
3126
3177
  { label: "Quit Relay", click: quitRelayCompletely },
3127
3178
  ]),
3128
3179
  );
@@ -3242,6 +3293,10 @@ ipcMain.handle("relay:contactsSearch", (_e, q) => groupCall((c) => c.searchConta
3242
3293
  ipcMain.handle("relay:accountInfo", () => accountInfo());
3243
3294
  ipcMain.handle("relay:pairWithCode", (_e, input) => pairWithCode(input));
3244
3295
  ipcMain.handle("relay:signOut", () => signOutAccount());
3296
+ ipcMain.handle("relay:updateNow", () => {
3297
+ runUpdateNow();
3298
+ return { ok: true };
3299
+ });
3245
3300
 
3246
3301
  // The overlay is normally focusable:false so it never steals keyboard focus from
3247
3302
  // Claude/Codex. Text fields (the contact form, quick reply) need focus, so the renderer
@@ -3434,6 +3489,10 @@ if (!gotSingleInstanceLock) {
3434
3489
  if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
3435
3490
  createWindow();
3436
3491
  createTray();
3492
+ // Update availability: one check after boot, then hourly. Cheap (a dist-tags
3493
+ // GET) and it only ever surfaces UI — the daemon still owns actual updating.
3494
+ setTimeout(() => checkForUpdate(), 20000).unref?.();
3495
+ setInterval(() => checkForUpdate(), 60 * 60 * 1000).unref?.();
3437
3496
  installActiveSpaceWatcher();
3438
3497
  installPowerAttentionLifecycle();
3439
3498
  // Display topology changes are the remaining event that can strand the
@@ -68,6 +68,8 @@ contextBridge.exposeInMainWorld("relay", {
68
68
  accountInfo: () => ipcRenderer.invoke("relay:accountInfo"),
69
69
  pairWithCode: (input) => ipcRenderer.invoke("relay:pairWithCode", input),
70
70
  signOut: () => ipcRenderer.invoke("relay:signOut"),
71
+ // Manual update backstop, when the auto-updater has not refreshed this machine.
72
+ updateNow: () => ipcRenderer.invoke("relay:updateNow"),
71
73
 
72
74
 
73
75
  // window plumbing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
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": {
package/src/mcp.js CHANGED
@@ -38,12 +38,12 @@ export const TOOLS = [
38
38
  title: {
39
39
  type: "string",
40
40
  description:
41
- "The concise, high-level human-facing takeaway. This is shown first and most prominently, so keep it to one clear phrase or sentence.",
41
+ "The human-readable layer of the relay, and the ONLY thing shown on the recipient's card — write it to carry real information, not to name a topic. In as few words as it honestly takes (aim ~8-20 words, hard stop around 140 characters), answer: what is this, why does this person care, is anything expected of them, and by when. Value-centric and human, never a detail dump: 'Postmark account still pending approval — DNS is ready, nothing needed from you until they reply' beats 'Postmark update'. State the ask explicitly when there is one, and say so plainly when there is not. The recipient reads this alone, decides whether to act, and only then opens the relay for the full detail in bodyMarkdown — so a title that merely labels the subject forces a pointless open.",
42
42
  },
43
43
  bodyMarkdown: {
44
44
  type: "string",
45
45
  description:
46
- "The complete human-facing detail you formulate from the sender's request and relevant context. Use readable Markdown (short paragraphs, headings, bullets, tables, or code where useful) and do not merely repeat the title. This is the message body, not a private recipient-agent briefing.",
46
+ "The high-detail layer, read AFTER the title earns the open. Formulate the complete human-facing detail from the sender's request and relevant context in readable Markdown (short paragraphs, headings, bullets, tables, or code where useful). Never merely repeat the title; the title already carried the what/why/ask, so this is the substance behind it. This is the message body, not a private recipient-agent briefing.",
47
47
  },
48
48
  userInstructions: { type: "string", description: "Optional private instruction for how the recipient's agent should handle a handoff." },
49
49
  targetSurfaces: {