relay-companion 0.1.61 → 0.1.63
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 +109 -55
- package/overlay/main.cjs +15 -0
- package/package.json +1 -1
- package/src/notifications.js +10 -1
package/overlay/inbox.html
CHANGED
|
@@ -241,9 +241,10 @@
|
|
|
241
241
|
.rk-subject {
|
|
242
242
|
font-family:var(--serif); font-size:15px; line-height:1.3; color:var(--muted); font-weight:400;
|
|
243
243
|
margin-top:5px;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
/* Two full lines with an ellipsis: titles must be scannable when choosing
|
|
245
|
+
which relay to open — the old right-edge fade cut them after a few words. */
|
|
246
|
+
overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
|
|
247
|
+
overflow-wrap:anywhere;
|
|
247
248
|
transition:color .28s var(--settle);
|
|
248
249
|
}
|
|
249
250
|
.row.unread .rk-subject { color:var(--ink); font-weight:500; }
|
|
@@ -560,11 +561,13 @@
|
|
|
560
561
|
.th-time { flex:0 0 auto; margin-left:auto; font-family:var(--mono); font-size:11px; color:var(--muted-2); font-variant-numeric:tabular-nums; padding-left:6px; }
|
|
561
562
|
.th-title {
|
|
562
563
|
font-family:var(--serif); font-size:14px; line-height:1.3; color:var(--muted); margin-top:4px;
|
|
563
|
-
|
|
564
|
-
-
|
|
565
|
-
mask-image:linear-gradient(to right, #000 calc(100% - 26px), transparent);
|
|
564
|
+
overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
|
|
565
|
+
overflow-wrap:anywhere;
|
|
566
566
|
}
|
|
567
567
|
.th-item.unread .th-title { color:var(--ink); font-weight:500; }
|
|
568
|
+
/* Conversations you started that have no reply yet sit quietly in the list. */
|
|
569
|
+
.th-item.th-sent-only .th-party { color:var(--muted); font-weight:500; }
|
|
570
|
+
.th-item.th-sent-only .th-title { color:var(--muted-2); }
|
|
568
571
|
.th-detail-head { display:flex; align-items:center; gap:9px; padding:10px 16px 8px; border-bottom:1px solid var(--hair-2); }
|
|
569
572
|
.th-back {
|
|
570
573
|
appearance:none; border:0; background:transparent; cursor:pointer; padding:0;
|
|
@@ -582,9 +585,8 @@
|
|
|
582
585
|
.th-msg-top { display:flex; align-items:baseline; gap:7px; min-width:0; }
|
|
583
586
|
.th-dir { flex:0 0 auto; font-family:var(--mono); font-size:11px; line-height:1; align-self:center; color:var(--muted-2); }
|
|
584
587
|
.th-dir.out { color:var(--accent); }
|
|
585
|
-
.th-msg-title { font-family:var(--serif); font-size:14px; line-height:1.3; color:var(--muted); margin-top:4px;
|
|
586
|
-
-webkit-
|
|
587
|
-
mask-image:linear-gradient(to right, #000 calc(100% - 26px), transparent); }
|
|
588
|
+
.th-msg-title { font-family:var(--serif); font-size:14px; line-height:1.3; color:var(--muted); margin-top:4px;
|
|
589
|
+
overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow-wrap:anywhere; }
|
|
588
590
|
.th-msg.unread .th-msg-title { color:var(--ink); font-weight:500; }
|
|
589
591
|
|
|
590
592
|
/* Sent-row read receipt chip: "read" sits in a light-green enclosure, "unread" stays quiet. */
|
|
@@ -630,7 +632,6 @@
|
|
|
630
632
|
<button class="tab active" type="button" data-view="relays">
|
|
631
633
|
Relays <span class="tab-badge gone" id="relaysBadge">0</span>
|
|
632
634
|
</button>
|
|
633
|
-
<button class="tab" type="button" data-view="threads">Threads</button>
|
|
634
635
|
<button class="tab" type="button" data-view="sent">Sent</button>
|
|
635
636
|
<button class="tab" type="button" data-view="contacts">Contacts</button>
|
|
636
637
|
<button class="mark-all-read gone" id="markAllRead" type="button">Mark all as read</button>
|
|
@@ -1428,51 +1429,87 @@
|
|
|
1428
1429
|
if (collapsed && !peeking) { pendingRelaysRender = true; return; }
|
|
1429
1430
|
pendingRelaysRender = false;
|
|
1430
1431
|
const allRows = (payload.relays || []).filter(isRelayListKind);
|
|
1431
|
-
|
|
1432
|
-
|
|
1432
|
+
if (peeking) {
|
|
1433
|
+
// Notification mode is unchanged: flat unread relay rows, newest first.
|
|
1434
|
+
const rows = allRows.filter((row) => row.unread);
|
|
1435
|
+
relaysEmptyEl.classList.toggle("gone", rows.length > 0);
|
|
1436
|
+
const seen = prevRelayIds;
|
|
1437
|
+
relaysListEl.innerHTML = rows.map((r) => relayRowHtml(r, seen)).join("");
|
|
1438
|
+
prevRelayIds = new Set(rows.map((r) => r.id));
|
|
1439
|
+
wireRelayRows();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
// Conversation list: everything is a thread; most threads have one message.
|
|
1443
|
+
// A single-inbound-message thread renders as the classic relay row (with the
|
|
1444
|
+
// expand-card open actions); anything with replies — or that only exists on
|
|
1445
|
+
// the sent side — renders as a conversation row that drills into the thread.
|
|
1446
|
+
const byId = new Map(allRows.map((r) => [r.id, r]));
|
|
1447
|
+
const threads = buildThreads();
|
|
1448
|
+
relaysEmptyEl.classList.toggle("gone", threads.length > 0);
|
|
1433
1449
|
const seen = prevRelayIds;
|
|
1434
|
-
relaysListEl.innerHTML =
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1440
|
-
// result / task_completed / plain message carry a body preview beneath the subject.
|
|
1441
|
-
// (human_question's body IS the subject, so no duplicate preview there.)
|
|
1442
|
-
const body = r.bodyMarkdown || r.body || "";
|
|
1443
|
-
const preview = kind === "human_question" ? "" : bodyPreview(body, subject, 110);
|
|
1444
|
-
const previewHtml = preview ? `<div class="rk-preview">${esc(preview)}</div>` : "";
|
|
1445
|
-
const canDelete = kind === "plain_relay" || kind === "human_question" || kind === "task_completed" || kind === "task_cancelled" || kind === "task_failed";
|
|
1446
|
-
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>` : "";
|
|
1447
|
-
// Row click toggles this open-actions card; the buttons carry the actual
|
|
1448
|
-
// opens ("current" injects into the live Claude session, "new" forces a
|
|
1449
|
-
// fresh host session).
|
|
1450
|
-
const expanded = r.id === expandedRelayId;
|
|
1451
|
-
const openCard = expanded ? `
|
|
1452
|
-
<div class="td-card row-open-card" data-stop="1">
|
|
1453
|
-
<div class="td-card-actions">
|
|
1454
|
-
<button class="td-btn primary" type="button" data-open-current="${esc(r.id)}">Open in current chat</button>
|
|
1455
|
-
<button class="td-btn ghost" type="button" data-open-fresh="${esc(r.id)}">Open in new chat</button>
|
|
1456
|
-
</div>
|
|
1457
|
-
</div>` : "";
|
|
1450
|
+
relaysListEl.innerHTML = threads.map((t) => {
|
|
1451
|
+
const inbound = t.msgs.filter((m) => m.direction === "in");
|
|
1452
|
+
if (t.msgs.length === 1 && inbound.length === 1 && byId.has(inbound[0].id)) {
|
|
1453
|
+
return relayRowHtml(byId.get(inbound[0].id), seen);
|
|
1454
|
+
}
|
|
1455
|
+
const sentOnly = inbound.length === 0;
|
|
1458
1456
|
return `
|
|
1459
|
-
<
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
<span class="
|
|
1463
|
-
|
|
1464
|
-
<span class="
|
|
1465
|
-
|
|
1466
|
-
</
|
|
1467
|
-
|
|
1468
|
-
${previewHtml}
|
|
1469
|
-
${actions}
|
|
1470
|
-
${openCard}
|
|
1471
|
-
<div class="row-err" data-err="${esc(r.id)}"></div>
|
|
1472
|
-
</div>`;
|
|
1457
|
+
<button class="th-item${t.unreadCount ? " unread" : ""}${sentOnly ? " th-sent-only" : ""}" type="button" data-thread="${esc(t.threadId)}">
|
|
1458
|
+
<span class="th-top">
|
|
1459
|
+
${t.unreadCount ? '<span class="th-unread"></span>' : ""}
|
|
1460
|
+
<span class="th-party">${sentOnly ? "To " : ""}${esc(t.party)}</span>
|
|
1461
|
+
${t.msgs.length > 1 ? `<span class="th-count">${t.msgs.length}</span>` : ""}
|
|
1462
|
+
<span class="th-time">${esc(timeAgo(t.latest.at))}</span>
|
|
1463
|
+
</span>
|
|
1464
|
+
<span class="th-title">${esc(t.latest.title)}</span>
|
|
1465
|
+
</button>`;
|
|
1473
1466
|
}).join("");
|
|
1474
|
-
prevRelayIds = new Set(
|
|
1467
|
+
prevRelayIds = new Set(allRows.map((r) => r.id));
|
|
1475
1468
|
wireRelayRows();
|
|
1469
|
+
for (const el of relaysListEl.querySelectorAll(".th-item")) {
|
|
1470
|
+
el.addEventListener("click", () => openThreadDetail(el.getAttribute("data-thread")));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// The classic relay row (used flat while peeking, and for single-message
|
|
1475
|
+
// conversations in the unified list). Row click toggles the open-actions
|
|
1476
|
+
// card; the buttons carry the actual opens.
|
|
1477
|
+
function relayRowHtml(r, seen) {
|
|
1478
|
+
const fresh = seen.size > 0 && !seen.has(r.id);
|
|
1479
|
+
const kind = r.relayNotificationKind;
|
|
1480
|
+
const subject = relaySubject(r);
|
|
1481
|
+
const sender = relaySender(r);
|
|
1482
|
+
const actions = relayActionsHtml(r);
|
|
1483
|
+
// result / task_completed / plain message carry a body preview beneath the subject.
|
|
1484
|
+
// (human_question's body IS the subject, so no duplicate preview there.)
|
|
1485
|
+
const body = r.bodyMarkdown || r.body || "";
|
|
1486
|
+
const preview = kind === "human_question" ? "" : bodyPreview(body, subject, 110);
|
|
1487
|
+
const previewHtml = preview ? `<div class="rk-preview">${esc(preview)}</div>` : "";
|
|
1488
|
+
const canDelete = kind === "plain_relay" || kind === "human_question" || kind === "task_completed" || kind === "task_cancelled" || kind === "task_failed";
|
|
1489
|
+
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>` : "";
|
|
1490
|
+
const expanded = r.id === expandedRelayId;
|
|
1491
|
+
const openCard = expanded ? `
|
|
1492
|
+
<div class="td-card row-open-card" data-stop="1">
|
|
1493
|
+
<div class="td-card-actions">
|
|
1494
|
+
<button class="td-btn primary" type="button" data-open-current="${esc(r.id)}">Open in current chat</button>
|
|
1495
|
+
<button class="td-btn ghost" type="button" data-open-fresh="${esc(r.id)}">Open in new chat</button>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>` : "";
|
|
1498
|
+
return `
|
|
1499
|
+
<div class="row ${r.unread ? "unread" : ""}${fresh ? " enter" : ""}${canDelete ? " deletable" : ""}${expanded ? " expanded" : ""}" data-id="${esc(r.id)}" data-opening-id="${esc(r.id)}">
|
|
1500
|
+
${deleteButton}
|
|
1501
|
+
<div class="rk-top">
|
|
1502
|
+
<span class="rk-dot"></span>
|
|
1503
|
+
<span class="rk-from">From</span>
|
|
1504
|
+
<span class="rk-sender">${esc(sender)}</span>
|
|
1505
|
+
<span class="rk-time">${timeAgo(r.createdAt)}</span>
|
|
1506
|
+
</div>
|
|
1507
|
+
<div class="rk-subject${kind === "human_question" ? " q" : ""}">${esc(subject)}</div>
|
|
1508
|
+
${previewHtml}
|
|
1509
|
+
${actions}
|
|
1510
|
+
${openCard}
|
|
1511
|
+
<div class="row-err" data-err="${esc(r.id)}"></div>
|
|
1512
|
+
</div>`;
|
|
1476
1513
|
}
|
|
1477
1514
|
|
|
1478
1515
|
// The white up-arrow inside the round prussian Send button.
|
|
@@ -2359,9 +2396,12 @@
|
|
|
2359
2396
|
unread: Boolean(r.unread),
|
|
2360
2397
|
});
|
|
2361
2398
|
}
|
|
2399
|
+
// A self-send exists on BOTH sides with the same relay id; keep only the
|
|
2400
|
+
// inbound copy (the richer local row) so the thread doesn't show doubles.
|
|
2401
|
+
const inboundIds = new Set(msgs.map((m) => m.id));
|
|
2362
2402
|
for (const s of payload.sent || []) {
|
|
2363
2403
|
const id = s.relayId || s.id;
|
|
2364
|
-
if (!id) continue;
|
|
2404
|
+
if (!id || inboundIds.has(id)) continue;
|
|
2365
2405
|
msgs.push({
|
|
2366
2406
|
id, threadId: s.threadId || id, direction: "out",
|
|
2367
2407
|
title: sentSubject(s), party: sentRecipient(s), at: s.createdAt,
|
|
@@ -2440,12 +2480,23 @@
|
|
|
2440
2480
|
}
|
|
2441
2481
|
thBackEl.addEventListener("click", () => {
|
|
2442
2482
|
threadDetailId = null;
|
|
2443
|
-
|
|
2483
|
+
activeView = "relays";
|
|
2484
|
+
syncTabs();
|
|
2485
|
+
applyView();
|
|
2486
|
+
renderAll();
|
|
2444
2487
|
});
|
|
2488
|
+
function openThreadDetail(threadId) {
|
|
2489
|
+
threadDetailId = threadId;
|
|
2490
|
+
activeView = "threads";
|
|
2491
|
+
syncTabs();
|
|
2492
|
+
applyView();
|
|
2493
|
+
}
|
|
2445
2494
|
|
|
2446
2495
|
// ---------- views / tabs ----------
|
|
2447
2496
|
function syncTabs() {
|
|
2448
|
-
|
|
2497
|
+
// taskDetail belongs to Sent; the thread-detail view belongs to Relays —
|
|
2498
|
+
// conversations ARE the Relays tab now, a thread is just its drill-down.
|
|
2499
|
+
const tabView = activeView === "taskDetail" ? "sent" : activeView === "threads" ? "relays" : activeView;
|
|
2449
2500
|
for (const tab of tabEls) tab.classList.toggle("active", tab.getAttribute("data-view") === tabView);
|
|
2450
2501
|
}
|
|
2451
2502
|
function applyView() {
|
|
@@ -2455,10 +2506,13 @@
|
|
|
2455
2506
|
taskDetailViewEl.classList.toggle("hidden", activeView !== "taskDetail");
|
|
2456
2507
|
contactsViewEl.classList.toggle("hidden", activeView !== "contacts");
|
|
2457
2508
|
if (activeView === "threads") {
|
|
2458
|
-
|
|
2509
|
+
// Entered only by clicking a conversation row (threadDetailId set there).
|
|
2459
2510
|
renderThreads();
|
|
2460
2511
|
if (window.relay.refreshSent) window.relay.refreshSent().catch(() => {}); // outbound half of threads
|
|
2461
2512
|
}
|
|
2513
|
+
if (activeView === "relays" && window.relay.refreshSent) {
|
|
2514
|
+
window.relay.refreshSent().catch(() => {}); // conversations include the sent side
|
|
2515
|
+
}
|
|
2462
2516
|
if (activeView !== "taskDetail" && taskDetailTimer) {
|
|
2463
2517
|
clearInterval(taskDetailTimer);
|
|
2464
2518
|
taskDetailTimer = null;
|
package/overlay/main.cjs
CHANGED
|
@@ -358,6 +358,20 @@ function isRelayTaskWebTarget(pathOrUrl) {
|
|
|
358
358
|
|
|
359
359
|
// ---- state.json reading (Relay attention rows) ---------------------------
|
|
360
360
|
|
|
361
|
+
// The daemon's staging ledger (~/.relay/task-ledger.json plainRelays map) must be
|
|
362
|
+
// invalidated whenever the packet store is reset, or its dedupe permanently blocks
|
|
363
|
+
// re-staging everything the wipe destroyed.
|
|
364
|
+
function clearPlainRelayLedger() {
|
|
365
|
+
try {
|
|
366
|
+
const dir = process.env.RELAY_CONFIG_DIR || path.join(os.homedir(), ".relay");
|
|
367
|
+
const ledgerPath = path.join(dir, "task-ledger.json");
|
|
368
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8")) || {};
|
|
369
|
+
ledger.plainRelays = {};
|
|
370
|
+
fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`);
|
|
371
|
+
console.error(`[overlay] ${new Date().toISOString()} cleared plainRelays ledger after store reset`);
|
|
372
|
+
} catch {}
|
|
373
|
+
}
|
|
374
|
+
|
|
361
375
|
function emptyStoreFor(current) {
|
|
362
376
|
return {
|
|
363
377
|
version: 1,
|
|
@@ -392,6 +406,7 @@ function scheduleAccountReset(current) {
|
|
|
392
406
|
if (!stillWrongUser && !stillWrongEmail) return; // a concurrent writer fixed it; wipe nothing
|
|
393
407
|
console.error(`[overlay] ${new Date().toISOString()} account mismatch: resetting local store`);
|
|
394
408
|
writeStateAtomic(emptyStoreFor(current));
|
|
409
|
+
clearPlainRelayLedger(); // a wiped store makes the dedupe ledger a lie: without this, the daemon never re-stages the server's relays and the inbox stays empty forever
|
|
395
410
|
});
|
|
396
411
|
} finally {
|
|
397
412
|
accountResetScheduled = false;
|
package/package.json
CHANGED
package/src/notifications.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { currentUser, readConfig, webUrl } from "./config.js";
|
|
4
|
+
import { currentUser, readConfig, taskLedgerPath, webUrl } from "./config.js";
|
|
5
5
|
import { relayBinPath } from "./install.js";
|
|
6
6
|
import { withJsonLock, withJsonLockStrict } from "./state-lock.cjs";
|
|
7
7
|
import inboxItemIds from "./inbox-item-id.cjs";
|
|
@@ -340,6 +340,15 @@ export function resetCompanionStateForAccount(
|
|
|
340
340
|
},
|
|
341
341
|
};
|
|
342
342
|
writeCompanionState(statePath, next);
|
|
343
|
+
// The staging ledger belongs to the store that was just destroyed. Left
|
|
344
|
+
// intact, its dedupe would permanently block re-staging every relay the
|
|
345
|
+
// reset wiped, leaving the inbox empty while the server still has them.
|
|
346
|
+
try {
|
|
347
|
+
const ledgerFile = taskLedgerPath();
|
|
348
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerFile, "utf8")) || {};
|
|
349
|
+
ledger.plainRelays = {};
|
|
350
|
+
fs.writeFileSync(ledgerFile, `${JSON.stringify(ledger, null, 2)}\n`);
|
|
351
|
+
} catch {}
|
|
343
352
|
return { reset: true, statePath };
|
|
344
353
|
});
|
|
345
354
|
}
|