relay-companion 0.1.42 → 0.1.44
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 +33 -1
- package/overlay/main.cjs +27 -0
- package/overlay/preload.cjs +1 -0
- package/package.json +1 -1
- package/src/client.js +12 -0
- package/src/mcp.js +45 -0
- package/src/notifications.js +32 -3
- package/src/task-daemon.js +4 -0
package/overlay/inbox.html
CHANGED
|
@@ -190,6 +190,16 @@
|
|
|
190
190
|
transition:background-color .14s var(--settle);
|
|
191
191
|
}
|
|
192
192
|
.row:hover { background:rgba(31,26,23,.025); }
|
|
193
|
+
.row-delete {
|
|
194
|
+
appearance:none; position:absolute; top:9px; right:9px; width:25px; height:25px;
|
|
195
|
+
display:flex; align-items:center; justify-content:center; border:0; border-radius:7px;
|
|
196
|
+
background:rgba(255,255,255,.86); color:var(--muted-3); cursor:pointer; opacity:0;
|
|
197
|
+
transition:opacity .14s var(--settle), color .14s var(--settle), background .14s var(--settle);
|
|
198
|
+
}
|
|
199
|
+
.row:hover .row-delete, .row-delete:focus-visible { opacity:1; }
|
|
200
|
+
.row-delete:hover { color:#a83a32; background:#fff1ef; }
|
|
201
|
+
.row-delete:disabled { opacity:.45; cursor:default; }
|
|
202
|
+
.row.deletable .rk-time { margin-right:22px; }
|
|
193
203
|
@keyframes rowEnter { from { opacity:0; transform:translateY(-12px); } to { opacity:1; transform:none; } }
|
|
194
204
|
.row.enter { animation:rowEnter .52s var(--soft) both; }
|
|
195
205
|
|
|
@@ -1248,8 +1258,11 @@
|
|
|
1248
1258
|
const body = r.bodyMarkdown || r.body || "";
|
|
1249
1259
|
const preview = kind === "human_question" ? "" : bodyPreview(body, subject, 110);
|
|
1250
1260
|
const previewHtml = preview ? `<div class="rk-preview">${esc(preview)}</div>` : "";
|
|
1261
|
+
const canDelete = kind === "plain_relay" || kind === "human_question" || kind === "task_completed" || kind === "task_cancelled" || kind === "task_failed";
|
|
1262
|
+
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>` : "";
|
|
1251
1263
|
return `
|
|
1252
|
-
<div class="row ${r.unread ? "unread" : ""}${fresh ? " enter" : ""}" data-id="${esc(r.id)}" data-opening-id="${esc(r.id)}">
|
|
1264
|
+
<div class="row ${r.unread ? "unread" : ""}${fresh ? " enter" : ""}${canDelete ? " deletable" : ""}" data-id="${esc(r.id)}" data-opening-id="${esc(r.id)}">
|
|
1265
|
+
${deleteButton}
|
|
1253
1266
|
<div class="rk-top">
|
|
1254
1267
|
<span class="rk-dot"></span>
|
|
1255
1268
|
<span class="rk-from">From</span>
|
|
@@ -1354,6 +1367,25 @@
|
|
|
1354
1367
|
ta.addEventListener("input", () => { ta.style.height = "auto"; ta.style.height = Math.min(84, ta.scrollHeight) + "px"; });
|
|
1355
1368
|
ta.addEventListener("mousedown", () => setInteractive(true));
|
|
1356
1369
|
}
|
|
1370
|
+
for (const btn of relaysListEl.querySelectorAll("[data-delete]")) {
|
|
1371
|
+
const id = btn.getAttribute("data-delete");
|
|
1372
|
+
btn.addEventListener("click", async (event) => {
|
|
1373
|
+
event.stopPropagation();
|
|
1374
|
+
if (btn.disabled) return;
|
|
1375
|
+
btn.disabled = true;
|
|
1376
|
+
showRowError(id, "");
|
|
1377
|
+
try {
|
|
1378
|
+
const res = await window.relay.deleteRelay(id);
|
|
1379
|
+
if (!res || res.ok === false) {
|
|
1380
|
+
showRowError(id, (res && res.error) || "Could not move this relay to Recently Deleted.");
|
|
1381
|
+
btn.disabled = false;
|
|
1382
|
+
}
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
showRowError(id, (err && err.message) || "Could not move this relay to Recently Deleted.");
|
|
1385
|
+
btn.disabled = false;
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1357
1389
|
// accept/reject are hidden with the Tasks surface. Only connector_reauth keeps a
|
|
1358
1390
|
// [data-review] "Reconnect" affordance here.
|
|
1359
1391
|
for (const btn of relaysListEl.querySelectorAll("[data-review]")) {
|
package/overlay/main.cjs
CHANGED
|
@@ -656,6 +656,32 @@ function ackPacket(packetId) {
|
|
|
656
656
|
pushInbox(true).catch(() => {});
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
+
async function deletePacket(packetId) {
|
|
660
|
+
if (!packetId) return { ok: false, error: "Missing relay id." };
|
|
661
|
+
const row = rowById(packetId);
|
|
662
|
+
if (!row) return { ok: true };
|
|
663
|
+
try {
|
|
664
|
+
// Server first: a transient network/API error must never make a recoverable
|
|
665
|
+
// item disappear locally without actually entering Recently Deleted.
|
|
666
|
+
const client = await relayClient();
|
|
667
|
+
await client.deleteInboxItem(packetId, { idempotencyKey: idempotencyKey("inbox_delete") });
|
|
668
|
+
withJsonLock(STATE_PATH, () => {
|
|
669
|
+
const store = readStore();
|
|
670
|
+
if (store.packets) delete store.packets[packetId];
|
|
671
|
+
writeStateAtomic(store);
|
|
672
|
+
});
|
|
673
|
+
presentedRelayIds.delete(packetId);
|
|
674
|
+
activeAttentionIds.delete(packetId);
|
|
675
|
+
writeOverlayPrefs();
|
|
676
|
+
await pushInbox(true);
|
|
677
|
+
return { ok: true };
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const message = error && error.message ? error.message : String(error);
|
|
680
|
+
console.error("[overlay] relay delete failed:", message);
|
|
681
|
+
return { ok: false, error: message };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
659
685
|
// ---- mutations -----------------------------------------------------------
|
|
660
686
|
|
|
661
687
|
function idempotencyKey(prefix) {
|
|
@@ -1598,6 +1624,7 @@ ipcMain.on("relay:openSent", (_e, id) => {
|
|
|
1598
1624
|
ipcMain.on("relay:openTask", (_e, taskId) => openTaskDetail(taskId));
|
|
1599
1625
|
ipcMain.on("relay:openUrl", (_e, url) => openUrlTarget(url));
|
|
1600
1626
|
ipcMain.on("relay:ack", (_e, id) => ackPacket(id));
|
|
1627
|
+
ipcMain.handle("relay:delete", (_e, id) => deletePacket(id));
|
|
1601
1628
|
ipcMain.handle("relay:refreshSent", async () => {
|
|
1602
1629
|
await refreshSent();
|
|
1603
1630
|
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
|
openSent: (id) => ipcRenderer.send("relay:openSent", id),
|
|
17
17
|
ack: (id) => ipcRenderer.send("relay:ack", id),
|
|
18
|
+
deleteRelay: (id) => ipcRenderer.invoke("relay:delete", id),
|
|
18
19
|
openUrl: (url) => ipcRenderer.send("relay:openUrl", url),
|
|
19
20
|
refreshSent: () => ipcRenderer.invoke("relay:refreshSent"),
|
|
20
21
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -73,6 +73,18 @@ export class RelayClient {
|
|
|
73
73
|
return this.#req("POST", `/v1/relays/${encodeURIComponent(id)}/read`, payload);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
deleteInboxItem(itemId, payload = {}) {
|
|
77
|
+
return this.#req("POST", `/v1/inbox-items/${encodeURIComponent(itemId)}/delete`, payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
recentlyDeleted() {
|
|
81
|
+
return this.#req("GET", "/v1/recently-deleted");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
restoreInboxItem(itemId, payload = {}) {
|
|
85
|
+
return this.#req("POST", `/v1/recently-deleted/${encodeURIComponent(itemId)}/restore`, payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
openRelay(token) {
|
|
77
89
|
return this.#req("GET", `/v1/open/${encodeURIComponent(token)}`, undefined, { auth: false });
|
|
78
90
|
}
|
package/src/mcp.js
CHANGED
|
@@ -94,6 +94,45 @@ export const TOOLS = [
|
|
|
94
94
|
"List ordinary Relay messages addressed to this human. This does not list task coordination relays; use relay_task_status for task state.",
|
|
95
95
|
inputSchema: { type: "object", properties: {} },
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: "relay_inbox_delete",
|
|
99
|
+
description:
|
|
100
|
+
"Move one Relay inbox item to Recently Deleted for this human. This immediately removes it from the Relay website inbox and companion pill, but does not erase the sender's Sent history and does not cancel, answer, or otherwise change an underlying Relay task. The item remains recoverable for exactly 30 days, then its recovery snapshot is permanently erased and restoration is impossible. For an ordinary relay, pass the relayId returned by relay_inbox_list. For a task question shown in the pill/site, pass human_question:<messageId>. For a terminal task notification, pass task_completed:<taskId>, task_cancelled:<taskId>, or task_failed:<taskId>. Do not guess an id or delete merely to acknowledge/mark handled; use relay_acknowledge for that ordinary-relay workflow. A pending human question may leave its agent task paused even after the inbox item is deleted, because deletion is only inbox organization. This operation is idempotent for a given item while it remains in Recently Deleted.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
itemId: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description:
|
|
107
|
+
"Exact stable inbox item id. Use relayId for ordinary relays, human_question:<messageId> for a task question, or task_<terminal-state>:<taskId> for a terminal task notification.",
|
|
108
|
+
},
|
|
109
|
+
idempotencyKey: { type: "string", description: "A unique key of at least 8 characters for this deletion request." },
|
|
110
|
+
},
|
|
111
|
+
required: ["itemId", "idempotencyKey"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "relay_recently_deleted_list",
|
|
116
|
+
description:
|
|
117
|
+
"Fetch this human's complete Recently Deleted Relay inbox. Use this before attempting a restore whenever the human describes an item by sender, title, content, or approximate time instead of giving an exact itemId. Results are ordered newest deletion first and include the exact itemId required by relay_recently_deleted_restore, source type, sender/title/body snapshot, deletion time, permanentlyDeletesAt, and daysRemaining. Recently Deleted exists only on the Relay website and through these MCP tools; it is intentionally never shown in the companion pill. Items remain here for 30 days from their most recent deletion. After permanentlyDeletesAt the recovery snapshot is erased, the item no longer appears here, and neither an agent nor Relay support can restore it. Listing is read-only and does not extend the 30-day deadline.",
|
|
118
|
+
inputSchema: { type: "object", properties: {} },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "relay_recently_deleted_restore",
|
|
122
|
+
description:
|
|
123
|
+
"Restore one exact inbox item from this human's Recently Deleted folder. First call relay_recently_deleted_list unless the human supplied an exact current itemId; match cautiously using sender, title, body, sourceType, and timestamps, and ask the human if multiple items could match. Never invent or infer an id. Restoration is only possible before permanentlyDeletesAt. A successful restore removes the item from Recently Deleted and makes it eligible to reappear in the website inbox and companion pill; the companion treats the restore as fresh delivery so an older item can notify again. Restoring a task question does not answer it, and may re-expose a still-paused run; restoring a task-status notification does not reopen the completed/cancelled/failed task. Restoring an ordinary relay does not alter the sender's Sent record. This operation is idempotent for the same already-restored item.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
itemId: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "The exact itemId returned by relay_recently_deleted_list. Do not use a title, relay subject, or guessed task/message id.",
|
|
130
|
+
},
|
|
131
|
+
idempotencyKey: { type: "string", description: "A unique key of at least 8 characters for this restore request." },
|
|
132
|
+
},
|
|
133
|
+
required: ["itemId", "idempotencyKey"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
97
136
|
{
|
|
98
137
|
name: "relay_acknowledge",
|
|
99
138
|
description:
|
|
@@ -456,6 +495,12 @@ export async function handleCall(client, name, args) {
|
|
|
456
495
|
);
|
|
457
496
|
case "relay_inbox_list":
|
|
458
497
|
return text(await client.inbox());
|
|
498
|
+
case "relay_inbox_delete":
|
|
499
|
+
return text(await client.deleteInboxItem(args.itemId, { idempotencyKey: args.idempotencyKey }));
|
|
500
|
+
case "relay_recently_deleted_list":
|
|
501
|
+
return text(await client.recentlyDeleted());
|
|
502
|
+
case "relay_recently_deleted_restore":
|
|
503
|
+
return text(await client.restoreInboxItem(args.itemId, { idempotencyKey: args.idempotencyKey }));
|
|
459
504
|
case "relay_acknowledge":
|
|
460
505
|
return text(await client.acknowledge(args.relayId, { idempotencyKey: args.idempotencyKey }));
|
|
461
506
|
case "relay_task_create":
|
package/src/notifications.js
CHANGED
|
@@ -113,7 +113,7 @@ function taskNotification(task, kind, title, body, url, extra = {}) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function buildHumanNotifications({ user, tasks = [], relays = [], connectors = [] } = {}) {
|
|
116
|
+
export function buildHumanNotifications({ user, tasks = [], relays = [], connectors = [], inboxState = {} } = {}) {
|
|
117
117
|
const notifications = [];
|
|
118
118
|
for (const task of tasks) {
|
|
119
119
|
const participant = participantForUser(task, user);
|
|
@@ -193,12 +193,22 @@ export function buildHumanNotifications({ user, tasks = [], relays = [], connect
|
|
|
193
193
|
};
|
|
194
194
|
notifications.push({ ...notification, action: actionForNotification(notification) });
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
const suppressed = new Set(inboxState.suppressedItemIds || []);
|
|
197
|
+
const restoredAtByItemId = inboxState.restoredAtByItemId || {};
|
|
198
|
+
return notifications
|
|
199
|
+
.filter((notification) => !suppressed.has(notification.id))
|
|
200
|
+
.map((notification) => ({
|
|
201
|
+
...notification,
|
|
202
|
+
deliveryVersion: restoredAtByItemId[notification.id] || "",
|
|
203
|
+
}));
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
export function freshNotifications(ledger, notifications) {
|
|
200
207
|
const seen = ledger.notifications || {};
|
|
201
|
-
return notifications.filter((notification) =>
|
|
208
|
+
return notifications.filter((notification) => {
|
|
209
|
+
const previous = seen[notification.id];
|
|
210
|
+
return !previous || (notification.deliveryVersion || "") !== (previous.deliveryVersion || "");
|
|
211
|
+
});
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
export function markNotificationsProcessed(ledger, notifications) {
|
|
@@ -207,6 +217,7 @@ export function markNotificationsProcessed(ledger, notifications) {
|
|
|
207
217
|
for (const notification of notifications) {
|
|
208
218
|
ledger.notifications[notification.id] = {
|
|
209
219
|
kind: notification.kind,
|
|
220
|
+
deliveryVersion: notification.deliveryVersion || "",
|
|
210
221
|
processedAt,
|
|
211
222
|
};
|
|
212
223
|
}
|
|
@@ -238,6 +249,24 @@ function writeCompanionState(statePath, state) {
|
|
|
238
249
|
fs.renameSync(tmp, statePath);
|
|
239
250
|
}
|
|
240
251
|
|
|
252
|
+
/** Apply server tombstones to rows already staged by this companion. */
|
|
253
|
+
export function removeSuppressedCompanionItems(itemIds, { statePath = companionStatePath() } = {}) {
|
|
254
|
+
const suppressed = new Set((itemIds || []).filter(Boolean).map(String));
|
|
255
|
+
if (!suppressed.size) return 0;
|
|
256
|
+
return withJsonLock(statePath, () => {
|
|
257
|
+
const state = readCompanionState(statePath);
|
|
258
|
+
state.packets ||= {};
|
|
259
|
+
let removed = 0;
|
|
260
|
+
for (const id of suppressed) {
|
|
261
|
+
if (!state.packets[id]) continue;
|
|
262
|
+
delete state.packets[id];
|
|
263
|
+
removed += 1;
|
|
264
|
+
}
|
|
265
|
+
if (removed) writeCompanionState(statePath, state);
|
|
266
|
+
return removed;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
241
270
|
function accountMarker({ user = currentUser(), deviceId = readConfig().deviceId || "" } = {}) {
|
|
242
271
|
return {
|
|
243
272
|
userId: user?.id || "",
|
package/src/task-daemon.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
defaultStageRelayCompanionItem,
|
|
15
15
|
freshNotifications,
|
|
16
16
|
markNotificationsProcessed,
|
|
17
|
+
removeSuppressedCompanionItems,
|
|
17
18
|
reconcileAnsweredQuestions,
|
|
18
19
|
stagePlainRelayItem as defaultStagePlainRelayItem,
|
|
19
20
|
} from "./notifications.js";
|
|
@@ -171,11 +172,14 @@ async function pollHumanNotifications({ client, ledger, stageCompanionItem, stag
|
|
|
171
172
|
client.listConnectors(),
|
|
172
173
|
]);
|
|
173
174
|
const events = await pollVisibleTaskEvents({ client, ledger, tasks, log });
|
|
175
|
+
const removed = removeSuppressedCompanionItems(relays.inboxState?.suppressedItemIds || []);
|
|
176
|
+
if (removed) log(`removed ${removed} Relay item(s) moved to Recently Deleted elsewhere`);
|
|
174
177
|
const notifications = buildHumanNotifications({
|
|
175
178
|
user: me.user,
|
|
176
179
|
tasks: tasks.tasks || [],
|
|
177
180
|
relays: relays.relays || [],
|
|
178
181
|
connectors: connectors.connectors || [],
|
|
182
|
+
inboxState: relays.inboxState || {},
|
|
179
183
|
});
|
|
180
184
|
const fresh = freshNotifications(ledger, notifications);
|
|
181
185
|
const staged = [];
|