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.
- package/overlay/inbox.html +37 -9
- package/overlay/main.cjs +59 -0
- 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
|
}
|
|
@@ -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
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
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 = "";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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: {
|