relay-companion 0.1.89 → 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.
- package/overlay/inbox.html +86 -14
- package/overlay/main.cjs +72 -6
- package/overlay/preload.cjs +2 -0
- package/package.json +1 -1
- package/src/mcp.js +2 -2
package/overlay/inbox.html
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
}
|
|
@@ -703,7 +703,8 @@
|
|
|
703
703
|
the reused .open-actions light-built panel — never from borders. */
|
|
704
704
|
.sv-head { display:flex; align-items:center; justify-content:space-between; padding:10px 16px 8px; }
|
|
705
705
|
.sv-title { font-family:var(--serif); font-size:16px; font-weight:500; color:var(--ink); }
|
|
706
|
-
|
|
706
|
+
/* First element in the pane now (no header above), so it owns the top inset. */
|
|
707
|
+
.sv-account { display:flex; align-items:center; gap:12px; padding:16px 16px 0; }
|
|
707
708
|
.sv-avatar {
|
|
708
709
|
flex:0 0 auto; width:38px; height:38px; border-radius:50%;
|
|
709
710
|
background:var(--accent-soft); color:var(--accent);
|
|
@@ -742,6 +743,8 @@
|
|
|
742
743
|
.sv-err { margin-top:8px; padding:0 16px; }
|
|
743
744
|
.sv-note { padding:10px 16px 0; font-size:11.5px; color:#1a7f37; line-height:1.4; }
|
|
744
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; }
|
|
745
748
|
/* signed-out call-to-action on the Relays empty state — quiet filled pill (th-count language) */
|
|
746
749
|
.sv-signin-banner {
|
|
747
750
|
appearance:none; border:0; cursor:pointer; margin-top:2px;
|
|
@@ -1561,6 +1564,10 @@
|
|
|
1561
1564
|
// The relay row whose open-actions card is expanded. Like openingIds, this
|
|
1562
1565
|
// survives the innerHTML rebuilds renderRelays performs on every push.
|
|
1563
1566
|
let expandedRelayId = null;
|
|
1567
|
+
// A conversation expands its actions IN PLACE, exactly like a single relay.
|
|
1568
|
+
// Taking over the whole pane (the old thread-detail push) lost the user's
|
|
1569
|
+
// place in the list for what is really a three-item menu.
|
|
1570
|
+
let expandedThreadId = null;
|
|
1564
1571
|
function openingTargets(id) {
|
|
1565
1572
|
return [...document.querySelectorAll("[data-opening-id]")].filter((e) => e.getAttribute("data-opening-id") === id);
|
|
1566
1573
|
}
|
|
@@ -1676,8 +1683,13 @@
|
|
|
1676
1683
|
if (t.msgs.length === 1 && inbound.length === 1 && byId.has(inbound[0].id)) {
|
|
1677
1684
|
return relayRowHtml(byId.get(inbound[0].id), seen);
|
|
1678
1685
|
}
|
|
1686
|
+
const tExpanded = t.threadId === expandedThreadId;
|
|
1687
|
+
const newestInbound = [...t.msgs].reverse().find((m) => m.direction === "in") || t.msgs[t.msgs.length - 1];
|
|
1688
|
+
const tMenu = tExpanded && newestInbound
|
|
1689
|
+
? openActionsHtml(newestInbound.id, { thread: true, threadId: t.threadId, messageCount: t.msgs.length })
|
|
1690
|
+
: "";
|
|
1679
1691
|
return `
|
|
1680
|
-
<div class="th-item${t.unreadCount ? " unread" : ""} deletable" role="button" tabindex="0" data-thread="${esc(t.threadId)}" data-party="${esc(t.party)}">
|
|
1692
|
+
<div class="th-item${t.unreadCount ? " unread" : ""}${tExpanded ? " expanded" : ""} deletable" role="button" tabindex="0" data-thread="${esc(t.threadId)}" data-party="${esc(t.party)}">
|
|
1681
1693
|
${threadDeleteButton(t)}
|
|
1682
1694
|
<span class="th-top">
|
|
1683
1695
|
${t.unreadCount ? '<span class="th-unread"></span>' : ""}
|
|
@@ -1686,13 +1698,21 @@
|
|
|
1686
1698
|
<span class="th-time">${esc(timeAgo(t.latest.at))}</span>
|
|
1687
1699
|
</span>
|
|
1688
1700
|
<span class="th-title">${esc(t.latest.title)}</span>
|
|
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>` : ""}
|
|
1689
1703
|
</div>`;
|
|
1690
1704
|
}).join("");
|
|
1691
1705
|
prevRelayIds = new Set(allRows.map((r) => r.id));
|
|
1692
1706
|
wireRelayRows();
|
|
1693
1707
|
for (const el of relaysListEl.querySelectorAll(".th-item")) {
|
|
1694
|
-
el.addEventListener("click", () =>
|
|
1708
|
+
el.addEventListener("click", () => {
|
|
1709
|
+
const id = el.getAttribute("data-thread");
|
|
1710
|
+
expandedThreadId = expandedThreadId === id ? null : id;
|
|
1711
|
+
expandedRelayId = null; // only one menu open at a time
|
|
1712
|
+
renderRelays();
|
|
1713
|
+
});
|
|
1695
1714
|
}
|
|
1715
|
+
wireInlineThreadActions();
|
|
1696
1716
|
}
|
|
1697
1717
|
|
|
1698
1718
|
// Grow the window as the user scrolls toward the end. Cheap: each extension
|
|
@@ -1711,7 +1731,7 @@
|
|
|
1711
1731
|
// Preview plus the two host-open actions, as one small text-first menu.
|
|
1712
1732
|
// Preview is the primary, non-navigation path; the other actions hand the
|
|
1713
1733
|
// relay to a current or fresh host session.
|
|
1714
|
-
function openActionsHtml(id, { thread, previewable = true } = {}) {
|
|
1734
|
+
function openActionsHtml(id, { thread, previewable = true, threadId = "", messageCount = 0 } = {}) {
|
|
1715
1735
|
const preview = thread ? `data-thread-preview="${esc(id)}"` : `data-preview="${esc(id)}"`;
|
|
1716
1736
|
const cur = thread ? `data-thread-open-current="${esc(id)}"` : `data-open-current="${esc(id)}"`;
|
|
1717
1737
|
const fresh = thread ? `data-thread-open-fresh="${esc(id)}"` : `data-open-fresh="${esc(id)}"`;
|
|
@@ -1724,9 +1744,37 @@
|
|
|
1724
1744
|
<button class="oa-item${previewable ? "" : " oa-primary"}" type="button" role="menuitem" ${cur}>${thread ? "Open Thread in Current Chat" : "Open in Current Chat"}</button>
|
|
1725
1745
|
<div class="oa-sep" role="separator"></div>
|
|
1726
1746
|
<button class="oa-item" type="button" role="menuitem" ${fresh}>${thread ? "Open Thread in New Chat" : "Open in New Chat"}</button>
|
|
1747
|
+
${threadId ? `<div class="oa-sep" role="separator"></div>
|
|
1748
|
+
<button class="oa-item" type="button" role="menuitem" data-thread-history="${esc(threadId)}">Read the Conversation${messageCount ? ` (${messageCount})` : ""}</button>` : ""}
|
|
1727
1749
|
</div>`;
|
|
1728
1750
|
}
|
|
1729
1751
|
|
|
1752
|
+
|
|
1753
|
+
// The inline conversation menu reuses the thread actions; scoped to the list so
|
|
1754
|
+
// the detail view's own copy keeps working unchanged.
|
|
1755
|
+
function wireInlineThreadActions() {
|
|
1756
|
+
for (const btn of relaysListEl.querySelectorAll("[data-thread-preview]")) {
|
|
1757
|
+
const id = btn.getAttribute("data-thread-preview");
|
|
1758
|
+
btn.addEventListener("click", () => {
|
|
1759
|
+
expandedThreadId = null;
|
|
1760
|
+
window.relay.preview(id);
|
|
1761
|
+
renderRelays();
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
for (const btn of relaysListEl.querySelectorAll("[data-thread-open-current]")) {
|
|
1765
|
+
btn.addEventListener("click", () => openRelayFromUI(btn.getAttribute("data-thread-open-current"), "relay", "current"));
|
|
1766
|
+
}
|
|
1767
|
+
for (const btn of relaysListEl.querySelectorAll("[data-thread-open-fresh]")) {
|
|
1768
|
+
btn.addEventListener("click", () => openRelayFromUI(btn.getAttribute("data-thread-open-fresh"), "relay", "fresh"));
|
|
1769
|
+
}
|
|
1770
|
+
for (const btn of relaysListEl.querySelectorAll("[data-thread-history]")) {
|
|
1771
|
+
btn.addEventListener("click", () => {
|
|
1772
|
+
const el = btn.closest(".th-item");
|
|
1773
|
+
openThreadDetail(btn.getAttribute("data-thread-history"), el ? el.getAttribute("data-party") || "" : "");
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1730
1778
|
// Conversations get the same trash affordance as single relays — the unified
|
|
1731
1779
|
// list made that inconsistent (a 1-message conversation renders as a relay row
|
|
1732
1780
|
// WITH delete; a 2-message one rendered as a conversation row without). One
|
|
@@ -1747,11 +1795,11 @@
|
|
|
1747
1795
|
const subject = relaySubject(r);
|
|
1748
1796
|
const sender = relaySender(r);
|
|
1749
1797
|
const actions = relayActionsHtml(r);
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const previewHtml =
|
|
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 = "";
|
|
1755
1803
|
const canDelete = kind === "plain_relay" || kind === "human_question" || kind === "task_completed" || kind === "task_cancelled" || kind === "task_failed";
|
|
1756
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>` : "";
|
|
1757
1805
|
const expanded = r.id === expandedRelayId;
|
|
@@ -2988,7 +3036,15 @@
|
|
|
2988
3036
|
const threadActions = threadOpenTarget
|
|
2989
3037
|
? openActionsHtml(threadOpenTarget.id, { thread: true, previewable: Boolean(newestInbound) })
|
|
2990
3038
|
: "";
|
|
2991
|
-
|
|
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) => `
|
|
2992
3048
|
<button class="th-msg${m.unread ? " unread" : ""}" type="button" data-msg="${esc(m.id)}" data-dir="${m.direction}">
|
|
2993
3049
|
<span class="th-msg-top">
|
|
2994
3050
|
<span class="th-dir${m.direction === "out" ? " out" : ""}">${m.direction === "out" ? "←" : "→"}</span>
|
|
@@ -3065,7 +3121,6 @@
|
|
|
3065
3121
|
if (info.paired) {
|
|
3066
3122
|
const name = info.name || info.email || "Relay account";
|
|
3067
3123
|
html += `
|
|
3068
|
-
<div class="sv-head"><span class="sv-title">Your account.</span></div>
|
|
3069
3124
|
<div class="sv-account">
|
|
3070
3125
|
<span class="sv-avatar">${esc(cvInitials(info.name, info.email))}</span>
|
|
3071
3126
|
<span class="sv-id">
|
|
@@ -3090,11 +3145,28 @@
|
|
|
3090
3145
|
if (svPairOpen) html += svPairRowHtml();
|
|
3091
3146
|
if (svNote) html += `<div class="sv-note">${esc(svNote)}</div>`;
|
|
3092
3147
|
html += `<div class="row-err sv-err" id="svErr">${esc(svError)}</div>`;
|
|
3093
|
-
|
|
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
|
+
}
|
|
3094
3158
|
settingsViewEl.innerHTML = html;
|
|
3095
3159
|
wireSettings();
|
|
3096
3160
|
}
|
|
3097
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
|
+
}
|
|
3098
3170
|
for (const z of settingsViewEl.querySelectorAll('[data-stop="1"]')) {
|
|
3099
3171
|
z.addEventListener("click", (e) => e.stopPropagation());
|
|
3100
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
|
}
|
|
@@ -2330,12 +2334,19 @@ function anchorTopRight() {
|
|
|
2330
2334
|
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) || screen.getPrimaryDisplay();
|
|
2331
2335
|
const wa = display.workArea;
|
|
2332
2336
|
const anchor = { x: wa.x + wa.width - WIN.width - MARGIN, y: wa.y + MARGIN, width: WIN.width, height: WIN.height };
|
|
2333
|
-
if (process.env.RELAY_OVERLAY_TEST === "1") {
|
|
2334
|
-
//
|
|
2335
|
-
//
|
|
2336
|
-
//
|
|
2337
|
-
//
|
|
2338
|
-
//
|
|
2337
|
+
if (process.env.RELAY_OVERLAY_TEST === "1" || process.env.RELAY_OVERLAY_PERF === "1") {
|
|
2338
|
+
// A harness run must not take over the developer's screen. Park sandbox
|
|
2339
|
+
// windows just off the bottom-right of the work area: still a REAL composited
|
|
2340
|
+
// window on the same display — so paint timing, dwell timers and hit tests
|
|
2341
|
+
// measure exactly what production does — but out of sight while the suite
|
|
2342
|
+
// runs, and never under the user's parked cursor (a pointer resting on the
|
|
2343
|
+
// card holds notifications open BY DESIGN, which wedged dwell scenarios).
|
|
2344
|
+
// RELAY_OVERLAY_TEST_ONSCREEN=1 puts it back on screen for eyeball debugging.
|
|
2345
|
+
if (process.env.RELAY_OVERLAY_TEST_ONSCREEN !== "1") {
|
|
2346
|
+
anchor.x = wa.x + wa.width - 8;
|
|
2347
|
+
anchor.y = wa.y + wa.height - 8;
|
|
2348
|
+
return anchor;
|
|
2349
|
+
}
|
|
2339
2350
|
try {
|
|
2340
2351
|
const p = screen.getCursorScreenPoint();
|
|
2341
2352
|
const overlaps =
|
|
@@ -3086,6 +3097,47 @@ function quitRelayCompletely() {
|
|
|
3086
3097
|
} catch {}
|
|
3087
3098
|
}
|
|
3088
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
|
+
|
|
3089
3141
|
function createTray() {
|
|
3090
3142
|
const icon = createTrayIcon();
|
|
3091
3143
|
if (!icon || icon.isEmpty()) {
|
|
@@ -3116,6 +3168,12 @@ function createTray() {
|
|
|
3116
3168
|
{ label: pillIsOnScreen() ? "Hide Relay" : "Show Relay", click: toggleFromTray },
|
|
3117
3169
|
...previewItem,
|
|
3118
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" },
|
|
3119
3177
|
{ label: "Quit Relay", click: quitRelayCompletely },
|
|
3120
3178
|
]),
|
|
3121
3179
|
);
|
|
@@ -3235,6 +3293,10 @@ ipcMain.handle("relay:contactsSearch", (_e, q) => groupCall((c) => c.searchConta
|
|
|
3235
3293
|
ipcMain.handle("relay:accountInfo", () => accountInfo());
|
|
3236
3294
|
ipcMain.handle("relay:pairWithCode", (_e, input) => pairWithCode(input));
|
|
3237
3295
|
ipcMain.handle("relay:signOut", () => signOutAccount());
|
|
3296
|
+
ipcMain.handle("relay:updateNow", () => {
|
|
3297
|
+
runUpdateNow();
|
|
3298
|
+
return { ok: true };
|
|
3299
|
+
});
|
|
3238
3300
|
|
|
3239
3301
|
// The overlay is normally focusable:false so it never steals keyboard focus from
|
|
3240
3302
|
// Claude/Codex. Text fields (the contact form, quick reply) need focus, so the renderer
|
|
@@ -3427,6 +3489,10 @@ if (!gotSingleInstanceLock) {
|
|
|
3427
3489
|
if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
|
|
3428
3490
|
createWindow();
|
|
3429
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?.();
|
|
3430
3496
|
installActiveSpaceWatcher();
|
|
3431
3497
|
installPowerAttentionLifecycle();
|
|
3432
3498
|
// Display topology changes are the remaining event that can strand the
|
package/overlay/preload.cjs
CHANGED
|
@@ -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
package/src/mcp.js
CHANGED
|
@@ -38,12 +38,12 @@ export const TOOLS = [
|
|
|
38
38
|
title: {
|
|
39
39
|
type: "string",
|
|
40
40
|
description:
|
|
41
|
-
"The
|
|
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
|
|
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: {
|