relay-companion 0.1.32 → 0.1.34

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.
@@ -241,6 +241,14 @@
241
241
  font-size:11.5px; line-height:1.4; color:var(--muted); margin-top:3px;
242
242
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
243
243
  }
244
+ .row.sent .rk-dot { background:var(--accent); border-color:var(--accent); opacity:.58; }
245
+ .row.sent.failed .rk-dot { background:#b4332a; border-color:#b4332a; opacity:.82; }
246
+ .row.sent.pending .rk-dot { background:transparent; border-color:var(--muted-3); opacity:1; }
247
+ .sent-body {
248
+ font-size:11.5px; line-height:1.45; color:var(--muted); margin-top:7px;
249
+ white-space:pre-line; display:-webkit-box; -webkit-box-orient:vertical;
250
+ -webkit-line-clamp:5; overflow:hidden;
251
+ }
244
252
 
245
253
  /* quick reply (human_question): rounded reply field + prussian round send */
246
254
  .qr { display:flex; gap:8px; margin-top:11px; align-items:flex-end; }
@@ -539,9 +547,7 @@
539
547
  <button class="tab active" type="button" data-view="relays">
540
548
  Relays <span class="tab-badge gone" id="relaysBadge">0</span>
541
549
  </button>
542
- <button class="tab" type="button" data-view="tasks">
543
- Tasks <span class="tab-badge gone" id="tasksBadge">0</span>
544
- </button>
550
+ <button class="tab" type="button" data-view="sent">Sent</button>
545
551
  <button class="tab" type="button" data-view="contacts">Contacts</button>
546
552
  </nav>
547
553
 
@@ -558,15 +564,15 @@
558
564
  </div>
559
565
  </section>
560
566
 
561
- <!-- Tasks view -->
562
- <section class="view hidden" id="tasksView">
563
- <div id="tasksList"></div>
564
- <div id="tasksEmpty" class="empty gone">
567
+ <!-- Sent view -->
568
+ <section class="view hidden" id="sentView">
569
+ <div id="sentList"></div>
570
+ <div id="sentEmpty" class="empty gone">
565
571
  <svg class="mark" width="22" height="22" viewBox="0 0 16 16" aria-hidden="true">
566
572
  <rect x="0" y="6" width="4" height="4"/><rect x="6" y="6" width="4" height="4"/><rect x="12" y="6" width="4" height="4"/>
567
573
  </svg>
568
- <div class="t1">No tasks yet</div>
569
- <div class="t2">Tasks you create or join will show their state here.</div>
574
+ <div class="t1">No sent relays yet</div>
575
+ <div class="t2">Relays you send will appear here with their delivery state.</div>
570
576
  </div>
571
577
  </section>
572
578
 
@@ -616,10 +622,12 @@
616
622
  const relaysListEl = document.getElementById("relaysList");
617
623
  const relaysEmptyEl = document.getElementById("relaysEmpty");
618
624
  const relaysBadgeEl = document.getElementById("relaysBadge");
619
- const tasksViewEl = document.getElementById("tasksView");
620
- const tasksListEl = document.getElementById("tasksList");
621
- const tasksEmptyEl = document.getElementById("tasksEmpty");
622
- const tasksBadgeEl = document.getElementById("tasksBadge");
625
+ const sentViewEl = document.getElementById("sentView");
626
+ const sentListEl = document.getElementById("sentList");
627
+ const sentEmptyEl = document.getElementById("sentEmpty");
628
+ const tasksViewEl = null;
629
+ const tasksListEl = document.createElement("div");
630
+ const tasksEmptyEl = document.createElement("div");
623
631
  const taskDetailViewEl = document.getElementById("taskDetailView");
624
632
  const contactsViewEl = document.getElementById("contactsView");
625
633
  const cvAddEl = document.getElementById("cvAdd");
@@ -763,8 +771,7 @@
763
771
  cardEl.classList.remove("collapsed", "offstage", "bye");
764
772
  ghost = true; peeking = true;
765
773
  cardEl.classList.add("peek", "ghost");
766
- const kind = row && row.relayNotificationKind;
767
- activeView = (kind === "task_request" || kind === "share_approval") ? "tasks" : "relays";
774
+ activeView = "relays";
768
775
  syncTabs();
769
776
  applyView();
770
777
  renderAll();
@@ -811,8 +818,7 @@
811
818
  return Math.min(44 + rowH + 6, 340);
812
819
  }
813
820
  // A new relay arrived. If folded, peek open like an iOS banner (~7s) then fold back.
814
- // The peek routes to the tab the item lives in: task_request / share_approval -> Tasks
815
- // (the clean split), everything else -> Relays.
821
+ // The peek routes to Relays. Task/share rows are hidden until that product surface is ready.
816
822
  function notifyArrival(row) {
817
823
  if (!collapsed && !peeking) return;
818
824
  if (peekTimer) clearTimeout(peekTimer);
@@ -820,8 +826,7 @@
820
826
  collapsed = false;
821
827
  cardEl.classList.remove("collapsed");
822
828
  cardEl.classList.add("peek");
823
- const kind = row && row.relayNotificationKind;
824
- activeView = (kind === "task_request" || kind === "share_approval") ? "tasks" : "relays";
829
+ activeView = "relays";
825
830
  syncTabs();
826
831
  applyView();
827
832
  renderAll();
@@ -1014,8 +1019,7 @@
1014
1019
  </div>`;
1015
1020
  }).join("");
1016
1021
  }
1017
- // The clean split: Relays shows ONLY the urgent quick-reply (human_question) and
1018
- // plain message/result/notice kinds. task_request + share_approval live in Tasks.
1022
+ // Tasks/share approvals are hidden from the pill until that product surface is ready.
1019
1023
  const RELAY_HIDDEN_KINDS = new Set(["task_request", "share_approval"]);
1020
1024
  function isRelayListKind(row) { return !RELAY_HIDDEN_KINDS.has(row && row.relayNotificationKind); }
1021
1025
 
@@ -1102,8 +1106,9 @@
1102
1106
 
1103
1107
  // ---------- state ----------
1104
1108
  let activeView = "relays";
1105
- let payload = { account: { paired: false }, ui: { canDismiss: true, reopenSurface: "the Relay icon" }, relays: [], tasks: [], contacts: [] };
1109
+ let payload = { account: { paired: false }, ui: { canDismiss: true, reopenSurface: "the Relay icon" }, relays: [], sent: [], tasks: [], contacts: [] };
1106
1110
  let prevRelayIds = new Set();
1111
+ const expandedSentIds = new Set();
1107
1112
  let contactsList = [];
1108
1113
  let cvEditing = null;
1109
1114
 
@@ -1207,8 +1212,7 @@
1207
1212
  const SEND_ARROW_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
1208
1213
 
1209
1214
  // The inline action affordances for each relay kind. task_request / share_approval
1210
- // never reach the Relays list (filtered out by isRelayListKind) — their accept/decline
1211
- // lives in the Tasks tab now.
1215
+ // never reach the Relays list while the Tasks product surface is hidden.
1212
1216
  function relayActionsHtml(r) {
1213
1217
  const k = r.relayNotificationKind;
1214
1218
  if (k === "human_question") {
@@ -1292,8 +1296,8 @@
1292
1296
  ta.addEventListener("input", () => { ta.style.height = "auto"; ta.style.height = Math.min(84, ta.scrollHeight) + "px"; });
1293
1297
  ta.addEventListener("mousedown", () => setInteractive(true));
1294
1298
  }
1295
- // accept/reject moved to the Tasks tab (the clean split). Only connector_reauth
1296
- // keeps a [data-review] "Reconnect" affordance here.
1299
+ // accept/reject are hidden with the Tasks surface. Only connector_reauth keeps a
1300
+ // [data-review] "Reconnect" affordance here.
1297
1301
  for (const btn of relaysListEl.querySelectorAll("[data-review]")) {
1298
1302
  const id = btn.getAttribute("data-review");
1299
1303
  btn.addEventListener("click", () => {
@@ -1334,6 +1338,92 @@
1334
1338
  }
1335
1339
  }
1336
1340
 
1341
+ // ---------- Sent view ----------
1342
+ function sentRecipient(r) {
1343
+ const rec = r && r.recipient ? r.recipient : {};
1344
+ return String(rec.name || rec.email || "Recipient").trim();
1345
+ }
1346
+ function sentSubject(r) {
1347
+ const title = String((r && (r.title || r.displayTitle)) || "").trim();
1348
+ return title || firstLine((r && r.bodyMarkdown) || "", 90) || "Relay";
1349
+ }
1350
+ function sentDeliveryLabel(r) {
1351
+ const d = (r && r.delivery) || {};
1352
+ const state = String(d.state || r.state || "").toLowerCase();
1353
+ const channel = String(d.channel || "").toLowerCase();
1354
+ if (state === "opened") return "Opened";
1355
+ if (state === "failed") return "Failed";
1356
+ if (state === "pending") return "Pending";
1357
+ if (channel === "email") return "Emailed";
1358
+ if (channel === "device") return "Sent to Relay";
1359
+ return "Sent";
1360
+ }
1361
+ function sentDetail(r) {
1362
+ const parts = [sentDeliveryLabel(r)];
1363
+ const d = (r && r.delivery) || {};
1364
+ const rec = (r && r.recipient) || {};
1365
+ if (d.openedAt) parts.push("opened " + timeAgo(d.openedAt));
1366
+ else if (d.sentAt) parts.push("sent " + timeAgo(d.sentAt));
1367
+ if (rec.email) parts.push(rec.email);
1368
+ const atts = Array.isArray(r && r.attachments) ? r.attachments : [];
1369
+ if (atts.length === 1) {
1370
+ const a = atts[0];
1371
+ const size = fmtBytes(a.bytes);
1372
+ parts.push(`${a.name || "attachment"}${size ? " · " + size : ""}`);
1373
+ } else if (atts.length > 1) {
1374
+ parts.push(`${atts.length} attachments`);
1375
+ }
1376
+ return parts.join(" · ");
1377
+ }
1378
+ function sentClass(r) {
1379
+ const state = String(((r && r.delivery) || {}).state || "").toLowerCase();
1380
+ if (state === "failed") return "failed";
1381
+ if (state === "pending") return "pending";
1382
+ return "";
1383
+ }
1384
+ function sentBodyText(r, max) {
1385
+ const text = plainMd((r && r.bodyMarkdown) || "", max || 700);
1386
+ return text || "";
1387
+ }
1388
+ function renderSent() {
1389
+ const rows = payload.sent || [];
1390
+ sentEmptyEl.classList.toggle("gone", rows.length > 0);
1391
+ if (!rows.length) { sentListEl.innerHTML = ""; return; }
1392
+ sentListEl.innerHTML = rows.map((r) => {
1393
+ const id = r.relayId || r.id || "";
1394
+ const subject = sentSubject(r);
1395
+ const body = r.bodyMarkdown || "";
1396
+ const preview = bodyPreview(body, subject, 118);
1397
+ const expanded = expandedSentIds.has(id);
1398
+ const detail = sentDetail(r);
1399
+ const klass = sentClass(r);
1400
+ return `
1401
+ <div class="row sent ${klass}${expanded ? " expanded" : ""}" data-sent-id="${esc(id)}">
1402
+ <div class="rk-top">
1403
+ <span class="rk-dot"></span>
1404
+ <span class="rk-from">To</span>
1405
+ <span class="rk-sender">${esc(sentRecipient(r))}</span>
1406
+ <span class="rk-time">${timeAgo(r.createdAt)}</span>
1407
+ </div>
1408
+ <div class="rk-subject">${esc(subject)}</div>
1409
+ ${preview ? `<div class="rk-preview">${esc(preview)}</div>` : ""}
1410
+ <div class="row-note">${esc(detail)}</div>
1411
+ ${expanded ? `<div class="sent-body">${esc(sentBodyText(r))}</div>` : ""}
1412
+ </div>`;
1413
+ }).join("");
1414
+ wireSentRows();
1415
+ }
1416
+ function wireSentRows() {
1417
+ for (const el of sentListEl.querySelectorAll(".row.sent")) {
1418
+ const id = el.getAttribute("data-sent-id");
1419
+ el.addEventListener("click", () => {
1420
+ if (expandedSentIds.has(id)) expandedSentIds.delete(id);
1421
+ else expandedSentIds.add(id);
1422
+ renderSent();
1423
+ });
1424
+ }
1425
+ }
1426
+
1337
1427
  // ---------- Tasks view ----------
1338
1428
  const TASK_GROUPS = [
1339
1429
  { key: "needs", label: "Needs you", match: (t) => t.requiresViewerAction },
@@ -1402,7 +1492,7 @@
1402
1492
  if (taskDetailTimer) { clearInterval(taskDetailTimer); taskDetailTimer = null; }
1403
1493
  taskDetailId = null;
1404
1494
  taskDetailData = null;
1405
- activeView = "tasks";
1495
+ activeView = "sent";
1406
1496
  syncTabs();
1407
1497
  applyView();
1408
1498
  renderAll();
@@ -1524,7 +1614,7 @@
1524
1614
  const t = taskDetailData;
1525
1615
  if (!t) {
1526
1616
  taskDetailViewEl.innerHTML = `
1527
- <div class="td-head"><button class="td-back" type="button" data-td-back="1">‹ Tasks</button></div>
1617
+ <div class="td-head"><button class="td-back" type="button" data-td-back="1">‹ Sent</button></div>
1528
1618
  <div class="td-quiet">${taskDetailError ? esc(taskDetailError) : "Loading task…"}</div>`;
1529
1619
  wireTaskDetail();
1530
1620
  return;
@@ -1542,7 +1632,7 @@
1542
1632
 
1543
1633
  let html = `
1544
1634
  <div class="td-head">
1545
- <button class="td-back" type="button" data-td-back="1">‹ Tasks</button>
1635
+ <button class="td-back" type="button" data-td-back="1">‹ Sent</button>
1546
1636
  <span class="td-when">${esc(timeAgo(t.updatedAt))}</span>
1547
1637
  </div>
1548
1638
  <div class="td-title">${esc(t.title || "Untitled task")}</div>
@@ -1896,13 +1986,12 @@
1896
1986
 
1897
1987
  // ---------- views / tabs ----------
1898
1988
  function syncTabs() {
1899
- // The in-pill task detail lives under the Tasks tab.
1900
- const tabView = activeView === "taskDetail" ? "tasks" : activeView;
1989
+ const tabView = activeView === "taskDetail" ? "sent" : activeView;
1901
1990
  for (const tab of tabEls) tab.classList.toggle("active", tab.getAttribute("data-view") === tabView);
1902
1991
  }
1903
1992
  function applyView() {
1904
1993
  relaysViewEl.classList.toggle("hidden", activeView !== "relays");
1905
- tasksViewEl.classList.toggle("hidden", activeView !== "tasks");
1994
+ sentViewEl.classList.toggle("hidden", activeView !== "sent");
1906
1995
  taskDetailViewEl.classList.toggle("hidden", activeView !== "taskDetail");
1907
1996
  contactsViewEl.classList.toggle("hidden", activeView !== "contacts");
1908
1997
  if (activeView !== "taskDetail" && taskDetailTimer) {
@@ -1915,8 +2004,8 @@
1915
2004
  cvFormEl.classList.add("hidden");
1916
2005
  if (window.relay.setFocusable) window.relay.setFocusable(false);
1917
2006
  }
1918
- if (activeView === "tasks") {
1919
- window.relay.refreshTasks().catch(() => {});
2007
+ if (activeView === "sent" && window.relay.refreshSent) {
2008
+ window.relay.refreshSent().catch(() => {});
1920
2009
  }
1921
2010
  }
1922
2011
  for (const tab of tabEls) {
@@ -1937,10 +2026,8 @@
1937
2026
  const unread = (payload.relays || []).filter((r) => r.unread && isRelayListKind(r)).length;
1938
2027
  setCount(unread);
1939
2028
  setBadge(relaysBadgeEl, unread);
1940
- const needsTasks = (payload.tasks || []).filter((t) => t.requiresViewerAction).length;
1941
- setBadge(tasksBadgeEl, needsTasks);
1942
2029
  if (activeView === "relays") renderRelays();
1943
- else if (activeView === "tasks") renderTasks();
2030
+ else if (activeView === "sent") renderSent();
1944
2031
  // contacts renders from its own load path
1945
2032
  }
1946
2033
 
@@ -1962,6 +2049,7 @@
1962
2049
  account: next.account || payload.account,
1963
2050
  ui: next.ui || payload.ui,
1964
2051
  relays: Array.isArray(next.relays) ? next.relays : [],
2052
+ sent: Array.isArray(next.sent) ? next.sent : [],
1965
2053
  tasks: Array.isArray(next.tasks) ? next.tasks : [],
1966
2054
  contacts: Array.isArray(next.contacts) ? next.contacts : (payload.contacts || []),
1967
2055
  };
@@ -1980,7 +2068,7 @@
1980
2068
  if (window.relay.onOpenDone) window.relay.onOpenDone((id) => stopOpening(id));
1981
2069
  window.relay.refresh().then((p) => {
1982
2070
  onPayload(p);
1983
- console.log("[overlay] booted ok, relays=" + (p && p.relays ? p.relays.length : 0) + " tasks=" + (p && p.tasks ? p.tasks.length : 0));
2071
+ console.log("[overlay] booted ok, relays=" + (p && p.relays ? p.relays.length : 0) + " sent=" + (p && p.sent ? p.sent.length : 0));
1984
2072
  }).catch((e) => console.log("[overlay] refresh failed", e && e.message ? e.message : e));
1985
2073
  </script>
1986
2074
  </body>
package/overlay/main.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // Relay companion pill — a small floating, always-on-top Relay window that lists
2
- // incoming Relay attention items + tasks, and lazy-opens them in Claude Code / Codex.
2
+ // incoming Relay attention items + sent relays, and lazy-opens them in Claude Code / Codex.
3
3
  //
4
4
  // Motion principle: the OS window is a FIXED-SIZE, transparent, click-through canvas.
5
5
  // It is NEVER resized while animating. All expand/collapse motion is GPU-composited CSS
@@ -15,7 +15,7 @@
15
15
  // Data:
16
16
  // - Relay attention rows come from ${RELAY_HOME:-~/.relay-companion}/state.json under
17
17
  // `state.packets` (staged by the companion daemon). Read it directly + fs.watch it.
18
- // - Tasks come live from RelayClient.listTasks(), refreshed on a timer and on demand.
18
+ // - Sent relays come live from RelayClient.sent(), refreshed on a timer and on demand.
19
19
  // - Contacts come live from RelayClient.listContacts() / upsertContact().
20
20
  // - Mutations (accept/reject/approve/decline/answer) call RelayClient with device-token
21
21
  // auth. ack/mark-read writes state.json directly (atomic temp+rename).
@@ -252,6 +252,30 @@ function readRelays() {
252
252
  });
253
253
  }
254
254
 
255
+ const RELAY_HIDDEN_KINDS = new Set(["task_request", "share_approval"]);
256
+ function visibleRelayRows(rows) {
257
+ return rows.filter((row) => !RELAY_HIDDEN_KINDS.has(row && row.relayNotificationKind));
258
+ }
259
+
260
+ // ---- sent relays (live) --------------------------------------------------
261
+
262
+ let sentCache = [];
263
+ let sentLoadedOnce = null;
264
+ async function refreshSent() {
265
+ try {
266
+ const client = await relayClient();
267
+ const res = await client.sent();
268
+ sentCache = Array.isArray(res && res.items) ? res.items : [];
269
+ } catch (error) {
270
+ console.error("[overlay] listSent failed:", error && error.message);
271
+ }
272
+ return sentCache;
273
+ }
274
+ function ensureSentLoaded() {
275
+ if (!sentLoadedOnce) sentLoadedOnce = refreshSent().catch(() => sentCache);
276
+ return sentLoadedOnce;
277
+ }
278
+
255
279
  // ---- tasks (live) --------------------------------------------------------
256
280
 
257
281
  let tasksCache = [];
@@ -316,7 +340,7 @@ async function saveContact(input) {
316
340
  // ---- payload assembly + push to renderer ---------------------------------
317
341
 
318
342
  async function buildPayload() {
319
- await ensureTasksLoaded();
343
+ await ensureSentLoaded();
320
344
  const [relays, contacts] = [readRelays(), await readContacts()];
321
345
  return {
322
346
  account: account(),
@@ -325,7 +349,8 @@ async function buildPayload() {
325
349
  reopenSurface: reopenSurfaceName(),
326
350
  },
327
351
  relays,
328
- tasks: tasksCache,
352
+ sent: sentCache,
353
+ tasks: [],
329
354
  contacts,
330
355
  };
331
356
  }
@@ -335,11 +360,12 @@ async function pushInbox(force) {
335
360
  if (!win || win.isDestroyed()) return;
336
361
  const payload = await buildPayload();
337
362
  const rows = payload.relays;
363
+ const notifiableRows = visibleRelayRows(rows);
338
364
  if (knownIds === null) {
339
- knownIds = new Set(rows.map((r) => r.id));
365
+ knownIds = new Set(notifiableRows.map((r) => r.id));
340
366
  } else {
341
- const fresh = rows.filter((r) => r.unread && !knownIds.has(r.id));
342
- knownIds = new Set(rows.map((r) => r.id));
367
+ const fresh = notifiableRows.filter((r) => r.unread && !knownIds.has(r.id));
368
+ knownIds = new Set(notifiableRows.map((r) => r.id));
343
369
  if (fresh.length && win && !win.isDestroyed()) {
344
370
  // newest arrival drives the peek. While dismissed, the same designed banner still
345
371
  // shows — as a "ghost": the window appears for the notification only, and the
@@ -357,13 +383,13 @@ async function pushInbox(force) {
357
383
  }
358
384
  const sig = JSON.stringify({
359
385
  relays: rows.map((r) => [r.id, r.relayNotificationKind, r.unread, r.title, r.senderName, r.urgency]),
360
- tasks: payload.tasks.map((t) => [
361
- t.id,
362
- t.state,
363
- t.updatedAt,
364
- t.requiresViewerAction,
365
- t.pendingApprovalCount,
366
- Array.isArray(t.participants) ? t.participants.length : 0,
386
+ sent: payload.sent.map((r) => [
387
+ r.relayId,
388
+ r.state,
389
+ r.updatedAt,
390
+ r.delivery && r.delivery.state,
391
+ r.delivery && r.delivery.channel,
392
+ r.hasAttachments,
367
393
  ]),
368
394
  contacts: payload.contacts.map((c) => [c.id, c.name, c.email]),
369
395
  account: [payload.account.paired, payload.account.email],
@@ -1034,7 +1060,7 @@ function createWindow() {
1034
1060
  }
1035
1061
  });
1036
1062
  win.once("ready-to-show", () => {
1037
- // buildPayload() awaits the first task load, so a single push paints all tabs.
1063
+ // buildPayload() awaits the first sent-relay load, so a single push paints all tabs.
1038
1064
  pushInbox(true).catch((error) => console.error("[overlay] initial push failed:", error && error.message));
1039
1065
  pollHosts();
1040
1066
  });
@@ -1049,10 +1075,10 @@ function createWindow() {
1049
1075
  } catch {}
1050
1076
  setInterval(() => pushInboxQuiet(), 2500); // safety net for state.json
1051
1077
  setInterval(() => {
1052
- refreshTasks()
1078
+ refreshSent()
1053
1079
  .then(() => pushInbox(false))
1054
1080
  .catch(() => {});
1055
- }, 5000); // live task refresh
1081
+ }, 5000); // live sent-relay refresh
1056
1082
  setInterval(pollHosts, 1500);
1057
1083
  }
1058
1084
 
@@ -1133,6 +1159,11 @@ ipcMain.on("relay:open", (_e, id) => openPacket(id));
1133
1159
  ipcMain.on("relay:openTask", (_e, taskId) => openTaskDetail(taskId));
1134
1160
  ipcMain.on("relay:openUrl", (_e, url) => openUrlTarget(url));
1135
1161
  ipcMain.on("relay:ack", (_e, id) => ackPacket(id));
1162
+ ipcMain.handle("relay:refreshSent", async () => {
1163
+ await refreshSent();
1164
+ await pushInbox(true);
1165
+ return sentCache;
1166
+ });
1136
1167
  ipcMain.handle("relay:refreshTasks", async () => {
1137
1168
  await refreshTasks();
1138
1169
  await pushInbox(true);
@@ -15,6 +15,7 @@ contextBridge.exposeInMainWorld("relay", {
15
15
  open: (id) => ipcRenderer.send("relay:open", id),
16
16
  ack: (id) => ipcRenderer.send("relay:ack", id),
17
17
  openUrl: (url) => ipcRenderer.send("relay:openUrl", url),
18
+ refreshSent: () => ipcRenderer.invoke("relay:refreshSent"),
18
19
 
19
20
  // task mutations (return { ok, error?, conflict? })
20
21
  accept: (taskId, participantId) => ipcRenderer.invoke("relay:accept", taskId, participantId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.js CHANGED
@@ -54,6 +54,10 @@ export class RelayClient {
54
54
  return this.#req("GET", "/v1/inbox");
55
55
  }
56
56
 
57
+ sent() {
58
+ return this.#req("GET", "/v1/sent");
59
+ }
60
+
57
61
  fetchRelay(id) {
58
62
  return this.#req("GET", `/v1/relays/${encodeURIComponent(id)}`);
59
63
  }
@@ -216,8 +216,9 @@ async function materializeRowInHost({ id, row, rowState = {}, host = "claude", l
216
216
  const cwd = resolveCwd();
217
217
  const rowWithAttachments = await materializeAttachmentFiles(row, { log });
218
218
  const materializationSignature = rowMaterializationSignature(rowWithAttachments);
219
+ const materializationSignatureKey = attachmentSignatureKeyForHost(cleanHost);
219
220
  const materializationStale =
220
- hasAttachments(rowWithAttachments) && rowState.attachmentMaterializationSignature !== materializationSignature;
221
+ hasAttachments(rowWithAttachments) && rowState[materializationSignatureKey] !== materializationSignature;
221
222
  // The human reads `visible`; the agent-only `operatorNote` (real ids + the tool
222
223
  // call to make) rides a hidden channel per host (Claude: a system-meta transcript
223
224
  // row; Codex: developerInstructions) so it never enters the visible transcript.
@@ -243,6 +244,7 @@ async function materializeRowInHost({ id, row, rowState = {}, host = "claude", l
243
244
  title: rowWithAttachments.displayTitle,
244
245
  attachments: rowWithAttachments.attachments || [],
245
246
  attachmentMaterializationSignature: materializationSignature,
247
+ [materializationSignatureKey]: materializationSignature,
246
248
  materializedSurfaces: {
247
249
  codex: true,
248
250
  claudeCode: Boolean(rowState.materializedSurfaces?.claudeCode || rowState.claudeNativeSession),
@@ -296,6 +298,7 @@ async function materializeRowInHost({ id, row, rowState = {}, host = "claude", l
296
298
  claudeNativeSession,
297
299
  attachments: rowWithAttachments.attachments || [],
298
300
  attachmentMaterializationSignature: materializationSignature,
301
+ [materializationSignatureKey]: materializationSignature,
299
302
  materializedSurfaces: {
300
303
  codex: Boolean(previousSurfaces.codex || rowState.threadId),
301
304
  claudeCode: Boolean(previousSurfaces.claudeCode || claude.nativeSession || claude.surfaces?.claudeCode),
@@ -399,6 +402,12 @@ function rowMaterializationSignature(row) {
399
402
  });
400
403
  }
401
404
 
405
+ function attachmentSignatureKeyForHost(host) {
406
+ return String(host || "").toLowerCase() === "codex"
407
+ ? "codexAttachmentMaterializationSignature"
408
+ : "claudeAttachmentMaterializationSignature";
409
+ }
410
+
402
411
  function hasAttachments(row) {
403
412
  return Array.isArray(row?.attachments) && row.attachments.length > 0;
404
413
  }