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.
- package/overlay/inbox.html +126 -38
- package/overlay/main.cjs +48 -17
- package/overlay/preload.cjs +1 -0
- package/package.json +1 -1
- package/src/client.js +4 -0
- package/src/materializer.js +10 -1
package/overlay/inbox.html
CHANGED
|
@@ -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="
|
|
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
|
-
<!--
|
|
562
|
-
<section class="view hidden" id="
|
|
563
|
-
<div id="
|
|
564
|
-
<div id="
|
|
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
|
|
569
|
-
<div class="t2">
|
|
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
|
|
620
|
-
const
|
|
621
|
-
const
|
|
622
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
1296
|
-
//
|
|
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 = "
|
|
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">‹
|
|
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">‹
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
1919
|
-
window.relay.
|
|
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 === "
|
|
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) + "
|
|
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 +
|
|
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
|
-
// -
|
|
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
|
|
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
|
-
|
|
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(
|
|
365
|
+
knownIds = new Set(notifiableRows.map((r) => r.id));
|
|
340
366
|
} else {
|
|
341
|
-
const fresh =
|
|
342
|
-
knownIds = new Set(
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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
|
-
|
|
1078
|
+
refreshSent()
|
|
1053
1079
|
.then(() => pushInbox(false))
|
|
1054
1080
|
.catch(() => {});
|
|
1055
|
-
}, 5000); // live
|
|
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);
|
package/overlay/preload.cjs
CHANGED
|
@@ -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
package/src/client.js
CHANGED
package/src/materializer.js
CHANGED
|
@@ -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
|
|
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
|
}
|