relay-companion 0.1.63 → 0.1.64
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/codex-open-current.cjs +2 -2
- package/overlay/inbox.html +16 -1
- package/overlay/main.cjs +17 -0
- package/package.json +1 -1
- package/src/claude-inject.cjs +17 -4
- package/src/client.js +4 -0
- package/src/materializer.js +34 -0
- package/src/mcp.js +18 -0
|
@@ -50,7 +50,7 @@ function defaultLog(...args) {
|
|
|
50
50
|
// silent dead end. `finish` (ack + spinner stop) fires exactly once for the
|
|
51
51
|
// bridge/automation tiers; the fresh tier's openPacket owns its own lifecycle.
|
|
52
52
|
async function openCodexInCurrent(
|
|
53
|
-
{ packetId, senderName = "", title = "" } = {},
|
|
53
|
+
{ packetId, senderName = "", title = "", threadId = null, threadCount = 0 } = {},
|
|
54
54
|
{
|
|
55
55
|
inject, // src/codex-inject.js
|
|
56
56
|
desktop, // src/codex-desktop.js
|
|
@@ -90,7 +90,7 @@ async function openCodexInCurrent(
|
|
|
90
90
|
`last active ${new Date(target.lastActiveAt).toISOString()})`,
|
|
91
91
|
);
|
|
92
92
|
|
|
93
|
-
const instruction = inject.buildInjectionInstruction({ relayId: packetId, senderName, title });
|
|
93
|
+
const instruction = inject.buildInjectionInstruction({ relayId: packetId, senderName, title, threadId, threadCount });
|
|
94
94
|
|
|
95
95
|
// ---- (a) bridge tier ----------------------------------------------------
|
|
96
96
|
if (!bridgeSubmitEnabled(env)) {
|
package/overlay/inbox.html
CHANGED
|
@@ -2462,7 +2462,18 @@
|
|
|
2462
2462
|
return renderThreads();
|
|
2463
2463
|
}
|
|
2464
2464
|
thDetailNameEl.textContent = thread.party;
|
|
2465
|
-
|
|
2465
|
+
// Thread-level opens act on the newest inbound message (falling back to the
|
|
2466
|
+
// newest message overall): "current" injects with the threadId so the agent
|
|
2467
|
+
// fetches the whole exchange; "new chat" seeds the full transcript.
|
|
2468
|
+
const newestInbound = [...thread.msgs].reverse().find((m) => m.direction === "in") || thread.msgs[thread.msgs.length - 1];
|
|
2469
|
+
const threadActions = newestInbound ? `
|
|
2470
|
+
<div class="td-card row-open-card" data-stop="1">
|
|
2471
|
+
<div class="td-card-actions">
|
|
2472
|
+
<button class="td-btn primary" type="button" data-thread-open-current="${esc(newestInbound.id)}">Open thread in current chat</button>
|
|
2473
|
+
<button class="td-btn ghost" type="button" data-thread-open-fresh="${esc(newestInbound.id)}">Open thread in new chat</button>
|
|
2474
|
+
</div>
|
|
2475
|
+
</div>` : "";
|
|
2476
|
+
thHistoryEl.innerHTML = threadActions + thread.msgs.map((m) => `
|
|
2466
2477
|
<button class="th-msg${m.unread ? " unread" : ""}" type="button" data-msg="${esc(m.id)}" data-dir="${m.direction}">
|
|
2467
2478
|
<span class="th-msg-top">
|
|
2468
2479
|
<span class="th-dir${m.direction === "out" ? " out" : ""}">${m.direction === "out" ? "←" : "→"}</span>
|
|
@@ -2477,6 +2488,10 @@
|
|
|
2477
2488
|
openRelayFromUI(id, el.getAttribute("data-dir") === "out" ? "sent" : "relay");
|
|
2478
2489
|
});
|
|
2479
2490
|
}
|
|
2491
|
+
const curBtn = thHistoryEl.querySelector("[data-thread-open-current]");
|
|
2492
|
+
if (curBtn) curBtn.addEventListener("click", () => openRelayFromUI(curBtn.getAttribute("data-thread-open-current"), "relay", "current"));
|
|
2493
|
+
const freshBtn = thHistoryEl.querySelector("[data-thread-open-fresh]");
|
|
2494
|
+
if (freshBtn) freshBtn.addEventListener("click", () => openRelayFromUI(freshBtn.getAttribute("data-thread-open-fresh"), "relay", "fresh"));
|
|
2480
2495
|
}
|
|
2481
2496
|
thBackEl.addEventListener("click", () => {
|
|
2482
2497
|
threadDetailId = null;
|
package/overlay/main.cjs
CHANGED
|
@@ -1508,6 +1508,21 @@ async function openPacket(packetId, { sent = false, fresh = false } = {}) {
|
|
|
1508
1508
|
// tools; the sender's content is never inlined (relay-briefing's
|
|
1509
1509
|
// untrusted-content discipline). A missing row or no live session/thread
|
|
1510
1510
|
// within 6h falls back to the fresh-open flow.
|
|
1511
|
+
// How many messages (inbound rows + sent copies, deduped) share a thread.
|
|
1512
|
+
// Gives the injected instruction its "N messages so far" and tells the model
|
|
1513
|
+
// when fetching the whole thread is worth it.
|
|
1514
|
+
function threadInfoFor(row) {
|
|
1515
|
+
const threadId = (row && row.threadId) || null;
|
|
1516
|
+
if (!threadId) return { threadId: null, threadCount: 0 };
|
|
1517
|
+
const ids = new Set();
|
|
1518
|
+
for (const r of readRelays()) if (r.threadId === threadId) ids.add(r.id);
|
|
1519
|
+
for (const s of sentCache || []) {
|
|
1520
|
+
const sid = s.relayId || s.id;
|
|
1521
|
+
if (sid && (s.threadId || sid) === threadId) ids.add(sid);
|
|
1522
|
+
}
|
|
1523
|
+
return { threadId, threadCount: ids.size };
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1511
1526
|
async function openPacketInCurrent(packetId) {
|
|
1512
1527
|
if (!packetId) return;
|
|
1513
1528
|
const row = rowById(packetId);
|
|
@@ -1539,6 +1554,7 @@ async function openPacketInCurrent(packetId) {
|
|
|
1539
1554
|
packetId,
|
|
1540
1555
|
senderName: row.senderName || "",
|
|
1541
1556
|
title: row.title || row.displayTitle || "",
|
|
1557
|
+
...threadInfoFor(row),
|
|
1542
1558
|
},
|
|
1543
1559
|
{
|
|
1544
1560
|
inject,
|
|
@@ -1574,6 +1590,7 @@ async function openPacketInCurrent(packetId) {
|
|
|
1574
1590
|
relayId: packetId,
|
|
1575
1591
|
senderName: row.senderName || "",
|
|
1576
1592
|
title: row.title || row.displayTitle || "",
|
|
1593
|
+
...threadInfoFor(row),
|
|
1577
1594
|
});
|
|
1578
1595
|
} catch (error) {
|
|
1579
1596
|
console.error("[overlay] claude injection staging failed:", error && error.message);
|
package/package.json
CHANGED
package/src/claude-inject.cjs
CHANGED
|
@@ -81,15 +81,28 @@ function sanitizeInline(value, cap = 140) {
|
|
|
81
81
|
return flat.length > cap ? `${flat.slice(0, cap - 1).trimEnd()}\u2026` : flat;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function buildInjectionInstruction({ relayId, senderName, title } = {}) {
|
|
84
|
+
function buildInjectionInstruction({ relayId, senderName, title, threadId, threadCount } = {}) {
|
|
85
85
|
const id = sanitizeInline(relayId, 80) || "unknown";
|
|
86
86
|
const sender = sanitizeInline(senderName) || "someone";
|
|
87
87
|
const subject = sanitizeInline(title) || "a relay";
|
|
88
|
-
|
|
88
|
+
const base =
|
|
89
89
|
`The user received relay ${id} from ${sender}: '${subject}'. ` +
|
|
90
90
|
"They clicked Open-in-current-chat. Fetch and open it now with the relay MCP tools " +
|
|
91
|
-
"(relay_inbox_list / GET the relay), then act on it."
|
|
92
|
-
);
|
|
91
|
+
"(relay_inbox_list / GET the relay), then act on it.";
|
|
92
|
+
const tid = sanitizeInline(threadId, 80);
|
|
93
|
+
const count = Number(threadCount) || 0;
|
|
94
|
+
if (tid && count > 1) {
|
|
95
|
+
return (
|
|
96
|
+
base +
|
|
97
|
+
` This relay is part of a conversation thread (threadId ${tid}, ${count} messages so far). ` +
|
|
98
|
+
"Call relay_thread_fetch with that threadId FIRST so the whole exchange is in your context, " +
|
|
99
|
+
"then act on the latest message in the thread."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (tid) {
|
|
103
|
+
return base + ` (Its threadId is ${tid}; if replies exist later, relay_thread_fetch retrieves the full conversation.)`;
|
|
104
|
+
}
|
|
105
|
+
return base;
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
// Stage one pending injection for a live session (or `sessionId: "any"` to
|
package/src/client.js
CHANGED
|
@@ -201,6 +201,10 @@ export class RelayClient {
|
|
|
201
201
|
return this.#req("POST", "/v1/tools/call", payload);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
thread(threadId) {
|
|
205
|
+
return this.#req("GET", `/v1/threads/${encodeURIComponent(threadId)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
204
208
|
searchContacts(q) {
|
|
205
209
|
return this.#req("GET", `/v1/contacts/search?q=${encodeURIComponent(q)}`);
|
|
206
210
|
}
|
package/src/materializer.js
CHANGED
|
@@ -149,9 +149,43 @@ async function resolveRow(id, { log = () => {}, allowTaskRows = false } = {}) {
|
|
|
149
149
|
// the next render echo the prior seed into itself (a doubled body). briefingMarkdown
|
|
150
150
|
// is preview-only and is not read back by the seed builders, so it is safe to set.
|
|
151
151
|
row.briefingMarkdown = renderRelayRowBriefing(row);
|
|
152
|
+
await prependThreadTranscript(row, { log });
|
|
152
153
|
return { row, rowState };
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
// "Open in new chat" on a relay that belongs to a multi-message thread must
|
|
157
|
+
// seed the WHOLE conversation — like opening a group chat — not just the one
|
|
158
|
+
// message. Fetch the thread from the API and prepend a chronological
|
|
159
|
+
// transcript to the briefing; on any failure the single-relay seed stands.
|
|
160
|
+
async function prependThreadTranscript(row, { log = () => {} } = {}) {
|
|
161
|
+
const threadId = row?.threadId || null;
|
|
162
|
+
if (!threadId) return;
|
|
163
|
+
try {
|
|
164
|
+
const { RelayClient } = await import("./client.js");
|
|
165
|
+
const client = new RelayClient();
|
|
166
|
+
const thread = await client.thread(threadId);
|
|
167
|
+
const msgs = Array.isArray(thread?.messages) ? thread.messages : Array.isArray(thread?.items) ? thread.items : [];
|
|
168
|
+
if (msgs.length < 2) return;
|
|
169
|
+
const lines = msgs.map((m) => {
|
|
170
|
+
const id = m.relayId || m.id || "";
|
|
171
|
+
const from = m.fromDisplayName || m.senderName || (m.direction === "outbound" ? "You" : "Them");
|
|
172
|
+
const when = m.createdAt || "";
|
|
173
|
+
const title = m.title || "";
|
|
174
|
+
const body = String(m.bodyMarkdown || m.preview || "").trim();
|
|
175
|
+
return `**${from}** — ${when} (relay ${id})\n${title ? `*${title}*\n` : ""}${body}`;
|
|
176
|
+
});
|
|
177
|
+
row.briefingMarkdown =
|
|
178
|
+
`## Conversation thread (${msgs.length} messages, oldest first — threadId ${threadId})\n\n` +
|
|
179
|
+
`${lines.join("\n\n---\n\n")}\n\n` +
|
|
180
|
+
`> The relay the user opened is the one addressed below. The full exchange above is context; ` +
|
|
181
|
+
`relay_thread_fetch with threadId ${threadId} re-fetches it at any time.\n\n---\n\n` +
|
|
182
|
+
row.briefingMarkdown;
|
|
183
|
+
log(`thread transcript seeded: ${msgs.length} messages from ${threadId}`);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
log(`thread transcript skipped: ${error?.message || error}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
async function resolvePublicOpenToken(token, { log = () => {} } = {}) {
|
|
156
190
|
try {
|
|
157
191
|
const { RelayClient } = await import("./client.js");
|
package/src/mcp.js
CHANGED
|
@@ -123,6 +123,21 @@ export const TOOLS = [
|
|
|
123
123
|
"List ordinary Relay messages addressed to this human. Each item carries threadId (the conversation thread it belongs to — the root relay's id) and, on replies, inReplyToRelayId; use them to reconstruct the recent exchange with a sender before continuing it via relay_send's inReplyToRelayId. This does not list task coordination relays; use relay_task_status for task state.",
|
|
124
124
|
inputSchema: { type: "object", properties: {} },
|
|
125
125
|
},
|
|
126
|
+
{
|
|
127
|
+
name: "relay_thread_fetch",
|
|
128
|
+
description:
|
|
129
|
+
"Fetch ONE full Relay conversation thread, oldest first: every relay in the exchange (both directions) with sender, recipients, timestamps, title, body, and read state. Use this whenever a relay you are opening or replying to is part of a thread (its threadId differs from its own id, or an Open-in-chat instruction names a threadId) so the WHOLE exchange is in context before you act — never reconstruct a conversation by fetching relays one at a time. Pass the threadId (the root relay's id, carried on every relay_inbox_list item).",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
threadId: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "The conversation thread id — the root relay's id, as carried on inbox items and sent responses.",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ["threadId"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
126
141
|
{
|
|
127
142
|
name: "relay_inbox_delete",
|
|
128
143
|
description:
|
|
@@ -479,6 +494,7 @@ export const MESSAGE_ONLY_TOOL_NAMES = new Set([
|
|
|
479
494
|
"relay_contact_update",
|
|
480
495
|
"relay_groups_list",
|
|
481
496
|
"relay_inbox_list",
|
|
497
|
+
"relay_thread_fetch",
|
|
482
498
|
"relay_acknowledge",
|
|
483
499
|
]);
|
|
484
500
|
|
|
@@ -548,6 +564,8 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
|
|
|
548
564
|
);
|
|
549
565
|
case "relay_inbox_list":
|
|
550
566
|
return text(await client.inbox());
|
|
567
|
+
case "relay_thread_fetch":
|
|
568
|
+
return text(await client.thread(args.threadId));
|
|
551
569
|
case "relay_inbox_delete":
|
|
552
570
|
return text(await client.deleteInboxItem(args.itemId, { idempotencyKey: args.idempotencyKey }));
|
|
553
571
|
case "relay_recently_deleted_list":
|