rogerrat 1.18.1 → 1.20.0
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/dist/app.js +23 -3
- package/dist/channel.js +49 -1
- package/dist/listen-here.js +51 -5
- package/dist/mcp.js +31 -7
- package/dist/remote-ui.js +36 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join as joinPath } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { streamSSE } from "hono/streaming";
|
|
7
|
-
import { ChannelError, setSessionTtlLookup, startPeriodicGc } from "./channel.js";
|
|
7
|
+
import { ChannelError, isPriority, setSessionTtlLookup, startPeriodicGc, validateSuggestedReplies, } from "./channel.js";
|
|
8
8
|
import { attachEmail, confirmEmailRecovery, createAccount, createIdentity, deleteIdentity, getAccount, getAccountIdsByIdentityCallsign, listIdentities, recoverAccount, removeEmail, requestEmailRecovery, verifyEmailCode, verifyIdentity, verifySession, } from "./accounts.js";
|
|
9
9
|
import { createChannelWebhook, createWebhook, deleteChannelWebhook, deleteWebhook, deliver, getActiveWebhooksForAccount, getActiveWebhooksForChannel, listChannelWebhooks, listWebhooks, } from "./webhooks.js";
|
|
10
10
|
import { buildRecoveryEmail, buildVerifyEmail, emailEnabled, sendEmail } from "./email.js";
|
|
@@ -743,6 +743,18 @@ export function createApp(opts) {
|
|
|
743
743
|
const to = String(body.to ?? "");
|
|
744
744
|
// Accept either `message` or `text` (transcripts return `text`, so clients reasonably try both).
|
|
745
745
|
const message = String(body.message ?? body.text ?? "");
|
|
746
|
+
// Optional ntfy-style priority. Server stores it; receivers decide what to do.
|
|
747
|
+
const priorityInput = body.priority;
|
|
748
|
+
if (priorityInput !== undefined && !isPriority(priorityInput)) {
|
|
749
|
+
return c.json({ error: "invalid priority; must be one of min|low|default|high|urgent", code: "invalid" }, 400);
|
|
750
|
+
}
|
|
751
|
+
let suggestedReplies;
|
|
752
|
+
try {
|
|
753
|
+
suggestedReplies = validateSuggestedReplies(body.suggested_replies);
|
|
754
|
+
}
|
|
755
|
+
catch (e) {
|
|
756
|
+
return handleChannelError(c, e);
|
|
757
|
+
}
|
|
746
758
|
const channel = getOrCreateChannel(channelId);
|
|
747
759
|
try {
|
|
748
760
|
const isBand = getChannelIsBand(channelId);
|
|
@@ -755,12 +767,20 @@ export function createApp(opts) {
|
|
|
755
767
|
retry_after_seconds: rate.retryAfter,
|
|
756
768
|
}, 429);
|
|
757
769
|
}
|
|
758
|
-
const msg = channel.send(sessionId, to, message);
|
|
770
|
+
const msg = channel.send(sessionId, to, message, priorityInput, suggestedReplies);
|
|
759
771
|
statsRecordMessage();
|
|
760
772
|
transcriptRecordMessage(channelId, getChannelRetention(channelId), msg);
|
|
761
773
|
fanoutWebhooks(channelId, msg);
|
|
762
774
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
763
|
-
return c.json({
|
|
775
|
+
return c.json({
|
|
776
|
+
ok: true,
|
|
777
|
+
id: msg.id,
|
|
778
|
+
at: msg.at,
|
|
779
|
+
queued,
|
|
780
|
+
to: msg.to,
|
|
781
|
+
...(msg.priority ? { priority: msg.priority } : {}),
|
|
782
|
+
...(msg.suggested_replies ? { suggested_replies: msg.suggested_replies } : {}),
|
|
783
|
+
});
|
|
764
784
|
}
|
|
765
785
|
catch (e) {
|
|
766
786
|
return handleChannelError(c, e);
|
package/dist/channel.js
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
export const PRIORITY_RANK = {
|
|
2
|
+
min: 0,
|
|
3
|
+
low: 1,
|
|
4
|
+
default: 2,
|
|
5
|
+
high: 3,
|
|
6
|
+
urgent: 4,
|
|
7
|
+
};
|
|
8
|
+
export function isPriority(v) {
|
|
9
|
+
return v === "min" || v === "low" || v === "default" || v === "high" || v === "urgent";
|
|
10
|
+
}
|
|
11
|
+
export const MAX_SUGGESTED_REPLIES = 4;
|
|
12
|
+
export const MAX_SUGGESTED_REPLY_LENGTH = 64;
|
|
13
|
+
/** Validate + normalize a suggested_replies array. Returns the cleaned array
|
|
14
|
+
* or throws a ChannelError describing what's wrong. Returns undefined if
|
|
15
|
+
* the input is undefined (the caller meant "no suggestions"). */
|
|
16
|
+
export function validateSuggestedReplies(v) {
|
|
17
|
+
if (v === undefined || v === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (!Array.isArray(v)) {
|
|
20
|
+
throw new ChannelError("suggested_replies must be an array of strings", "invalid", 400);
|
|
21
|
+
}
|
|
22
|
+
if (v.length === 0)
|
|
23
|
+
return undefined; // empty array = same as omitted
|
|
24
|
+
if (v.length > MAX_SUGGESTED_REPLIES) {
|
|
25
|
+
throw new ChannelError(`suggested_replies: max ${MAX_SUGGESTED_REPLIES} entries (got ${v.length})`, "invalid", 400);
|
|
26
|
+
}
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const item of v) {
|
|
29
|
+
if (typeof item !== "string") {
|
|
30
|
+
throw new ChannelError("suggested_replies entries must all be strings", "invalid", 400);
|
|
31
|
+
}
|
|
32
|
+
const trimmed = item.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
continue; // skip empty strings silently
|
|
35
|
+
if (trimmed.length > MAX_SUGGESTED_REPLY_LENGTH) {
|
|
36
|
+
throw new ChannelError(`suggested_replies entry too long (max ${MAX_SUGGESTED_REPLY_LENGTH} chars)`, "invalid", 400);
|
|
37
|
+
}
|
|
38
|
+
out.push(trimmed);
|
|
39
|
+
}
|
|
40
|
+
return out.length > 0 ? out : undefined;
|
|
41
|
+
}
|
|
1
42
|
const HISTORY_CAP = 100;
|
|
2
43
|
// Default idle TTL; channels can override via session_ttl_seconds at creation (max 24h).
|
|
3
44
|
const DEFAULT_ROSTER_IDLE_MS = 30 * 60 * 1000;
|
|
@@ -193,7 +234,7 @@ export class Channel {
|
|
|
193
234
|
sessionExists(sessionId) {
|
|
194
235
|
return this.callsignBySession.has(sessionId);
|
|
195
236
|
}
|
|
196
|
-
send(sessionId, to, text) {
|
|
237
|
+
send(sessionId, to, text, priority, suggestedReplies) {
|
|
197
238
|
this.ensureJoined(sessionId);
|
|
198
239
|
const from = this.callsignBySession.get(sessionId);
|
|
199
240
|
const dest = this.resolveAddress(to);
|
|
@@ -214,6 +255,13 @@ export class Channel {
|
|
|
214
255
|
const now = Date.now();
|
|
215
256
|
this.nextMsgId = Math.max(now, this.nextMsgId + 1);
|
|
216
257
|
const msg = { id: this.nextMsgId, from, to: dest, text, at: now };
|
|
258
|
+
// Only attach `priority` when explicitly non-default — keeps the wire format
|
|
259
|
+
// backward-compatible for consumers that don't know about priorities.
|
|
260
|
+
if (priority && priority !== "default")
|
|
261
|
+
msg.priority = priority;
|
|
262
|
+
if (suggestedReplies && suggestedReplies.length > 0) {
|
|
263
|
+
msg.suggested_replies = suggestedReplies;
|
|
264
|
+
}
|
|
217
265
|
this.messages.push(msg);
|
|
218
266
|
if (this.messages.length > HISTORY_CAP)
|
|
219
267
|
this.messages.shift();
|
package/dist/listen-here.js
CHANGED
|
@@ -35,16 +35,25 @@ options:
|
|
|
35
35
|
--origin <url> RogerRat origin (default: https://rogerrat.chat)
|
|
36
36
|
--since <msg_id> resume from a known message id (skips per-session cursor)
|
|
37
37
|
--on-message <cmd> shell command to run for each delivered message; env vars
|
|
38
|
-
RR_MESSAGE, RR_FROM, RR_TO, RR_MSG_ID, RR_CHANNEL
|
|
38
|
+
RR_MESSAGE, RR_FROM, RR_TO, RR_MSG_ID, RR_CHANNEL,
|
|
39
|
+
RR_PRIORITY, RR_REPLIES are set
|
|
40
|
+
(RR_PRIORITY = "default" when omitted;
|
|
41
|
+
RR_REPLIES = tab-separated suggested replies, empty if none)
|
|
39
42
|
--inbox <file> append each message to this file (parent dir created if
|
|
40
43
|
missing). Format controlled by --format.
|
|
41
44
|
--format <fmt> output format for stdout and --inbox lines:
|
|
42
|
-
jsonl (default) — one JSON object per line: {id,from,to,text,at}
|
|
43
|
-
text — one line per message:
|
|
44
|
-
|
|
45
|
+
jsonl (default) — one JSON object per line: {id,from,to,text,at,priority?,suggested_replies?}
|
|
46
|
+
text — one line per message:
|
|
47
|
+
"[<priority>] [<from>] <text> → [r1] [r2]"
|
|
48
|
+
where [<priority>] and → [r1] [r2] only appear
|
|
49
|
+
when set. Newlines in text collapsed to spaces.
|
|
45
50
|
Use this when feeding a Monitor/tail consumer
|
|
46
51
|
so each line is a human-readable notification
|
|
47
52
|
with no parser needed in the watcher.
|
|
53
|
+
--min-priority <p> drop messages below this priority (min|low|default|high|urgent).
|
|
54
|
+
Filtered messages do NOT write to --inbox, fire --on-message,
|
|
55
|
+
or print to stdout. Use for park-style channels: stay
|
|
56
|
+
wake-able only on real signals, let chatter accumulate silently.
|
|
48
57
|
--quiet suppress the default stdout dump of each message
|
|
49
58
|
|
|
50
59
|
if neither --on-message nor --inbox is given, messages print to stdout (one
|
|
@@ -70,6 +79,13 @@ examples:
|
|
|
70
79
|
rogerrat listen-here --channel ch1 --token t --session s \\
|
|
71
80
|
--on-message 'claude -p "rogerrat msg from $RR_FROM: $RR_MESSAGE"'
|
|
72
81
|
`;
|
|
82
|
+
const PRIORITY_RANK = {
|
|
83
|
+
min: 0,
|
|
84
|
+
low: 1,
|
|
85
|
+
default: 2,
|
|
86
|
+
high: 3,
|
|
87
|
+
urgent: 4,
|
|
88
|
+
};
|
|
73
89
|
function parseFlags(argv) {
|
|
74
90
|
let parsed;
|
|
75
91
|
try {
|
|
@@ -84,6 +100,7 @@ function parseFlags(argv) {
|
|
|
84
100
|
"on-message": { type: "string" },
|
|
85
101
|
inbox: { type: "string" },
|
|
86
102
|
format: { type: "string" },
|
|
103
|
+
"min-priority": { type: "string" },
|
|
87
104
|
quiet: { type: "boolean" },
|
|
88
105
|
help: { type: "boolean", short: "h" },
|
|
89
106
|
},
|
|
@@ -116,6 +133,14 @@ function parseFlags(argv) {
|
|
|
116
133
|
}
|
|
117
134
|
format = parsed.values.format;
|
|
118
135
|
}
|
|
136
|
+
let minPriority;
|
|
137
|
+
if (parsed.values["min-priority"] !== undefined) {
|
|
138
|
+
const v = parsed.values["min-priority"];
|
|
139
|
+
if (v !== "min" && v !== "low" && v !== "default" && v !== "high" && v !== "urgent") {
|
|
140
|
+
return { error: "--min-priority must be one of min|low|default|high|urgent" };
|
|
141
|
+
}
|
|
142
|
+
minPriority = v;
|
|
143
|
+
}
|
|
119
144
|
return {
|
|
120
145
|
channel,
|
|
121
146
|
token,
|
|
@@ -126,6 +151,7 @@ function parseFlags(argv) {
|
|
|
126
151
|
inbox: parsed.values.inbox,
|
|
127
152
|
format,
|
|
128
153
|
quiet: parsed.values.quiet === true,
|
|
154
|
+
minPriority,
|
|
129
155
|
};
|
|
130
156
|
}
|
|
131
157
|
/** Yield one SSE block (the lines between two blank-line separators) at a time.
|
|
@@ -169,11 +195,27 @@ function formatLine(args, msg) {
|
|
|
169
195
|
// one notification per message. Use a single space; the JSONL format is
|
|
170
196
|
// available for callers that need lossless body content.
|
|
171
197
|
const flat = msg.text.replace(/\r?\n/g, " ").trim();
|
|
172
|
-
|
|
198
|
+
// Surface non-default priority as a leading tag so a Monitor tail of the
|
|
199
|
+
// inbox file can grep for urgency without parsing JSON. e.g. `[urgent] `
|
|
200
|
+
const prio = msg.priority && msg.priority !== "default" ? `[${msg.priority}] ` : "";
|
|
201
|
+
// Surface suggested_replies as a trailing → [yes] [no] hint so the
|
|
202
|
+
// agent sees the canned options without parsing JSON.
|
|
203
|
+
const replies = msg.suggested_replies && msg.suggested_replies.length > 0
|
|
204
|
+
? " → " + msg.suggested_replies.map((r) => `[${r}]`).join(" ")
|
|
205
|
+
: "";
|
|
206
|
+
return `${prio}[${msg.from}] ${flat}${replies}`;
|
|
173
207
|
}
|
|
174
208
|
return JSON.stringify(msg);
|
|
175
209
|
}
|
|
176
210
|
async function dispatch(args, msg) {
|
|
211
|
+
// --min-priority filter: drop messages below the threshold entirely (no
|
|
212
|
+
// inbox write, no hook spawn, no stdout). Missing priority counts as
|
|
213
|
+
// "default" (rank 2).
|
|
214
|
+
if (args.minPriority) {
|
|
215
|
+
const incomingRank = PRIORITY_RANK[msg.priority ?? "default"];
|
|
216
|
+
if (incomingRank < PRIORITY_RANK[args.minPriority])
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
177
219
|
const line = formatLine(args, msg);
|
|
178
220
|
if (args.inbox) {
|
|
179
221
|
const dir = dirname(args.inbox);
|
|
@@ -199,6 +241,10 @@ async function dispatch(args, msg) {
|
|
|
199
241
|
RR_TO: msg.to,
|
|
200
242
|
RR_MSG_ID: String(msg.id),
|
|
201
243
|
RR_CHANNEL: args.channel,
|
|
244
|
+
RR_PRIORITY: msg.priority ?? "default",
|
|
245
|
+
// Tab-separated so the hook can split on $'\t' cleanly even if a reply
|
|
246
|
+
// contains spaces or punctuation. Empty string when no suggestions.
|
|
247
|
+
RR_REPLIES: (msg.suggested_replies ?? []).join("\t"),
|
|
202
248
|
},
|
|
203
249
|
});
|
|
204
250
|
child.on("error", (err) => {
|
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createAccount, createIdentity as accountCreateIdentity, verifyIdentity, verifySession } from "./accounts.js";
|
|
3
|
-
import { getOrCreateChannel } from "./channel.js";
|
|
3
|
+
import { getOrCreateChannel, isPriority, validateSuggestedReplies, } from "./channel.js";
|
|
4
4
|
import { buildConnectInfo } from "./connect.js";
|
|
5
5
|
import { createRemoteControl } from "./remote-control.js";
|
|
6
6
|
import { getPreset } from "./presets.js";
|
|
@@ -62,12 +62,17 @@ const CHANNEL_TOOLS = [
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: "send",
|
|
65
|
-
description: "Send a message to another agent on the channel by their callsign, or to 'all' to broadcast. Returns the message id.",
|
|
65
|
+
description: "Send a message to another agent on the channel by their callsign, or to 'all' to broadcast. Returns the message id. Optional `priority` (min|low|default|high|urgent) — receivers may wake immediately on high/urgent.",
|
|
66
66
|
inputSchema: {
|
|
67
67
|
type: "object",
|
|
68
68
|
properties: {
|
|
69
69
|
to: { type: "string", description: "Recipient callsign, or 'all' for broadcast." },
|
|
70
70
|
message: { type: "string", description: "Message text. Max 8192 chars." },
|
|
71
|
+
priority: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["min", "low", "default", "high", "urgent"],
|
|
74
|
+
description: "Optional urgency. Default = 'default'. Receivers interpret.",
|
|
75
|
+
},
|
|
71
76
|
},
|
|
72
77
|
required: ["to", "message"],
|
|
73
78
|
},
|
|
@@ -164,12 +169,23 @@ const UNIFIED_TOOLS = [
|
|
|
164
169
|
},
|
|
165
170
|
{
|
|
166
171
|
name: "send",
|
|
167
|
-
description: "Send a message to another agent on the channel you joined, or to 'all' to broadcast. Requires a prior join() in this session. The 'to' field accepts: a callsign ('front'), an index ('#1' or '1') from roster(), or 'all'.",
|
|
172
|
+
description: "Send a message to another agent on the channel you joined, or to 'all' to broadcast. Requires a prior join() in this session. The 'to' field accepts: a callsign ('front'), an index ('#1' or '1') from roster(), or 'all'. Optional `priority` tags urgency (min|low|default|high|urgent). Optional `suggested_replies` hints up to 4 canned replies that human-in-the-loop UIs (like the /remote phone view) render as tappable chips — agent receivers can read them too and pick one.",
|
|
168
173
|
inputSchema: {
|
|
169
174
|
type: "object",
|
|
170
175
|
properties: {
|
|
171
176
|
to: { type: "string", description: "Recipient: callsign, '#N' index, or 'all' for broadcast." },
|
|
172
177
|
message: { type: "string", description: "Message text. Max 8192 chars." },
|
|
178
|
+
priority: {
|
|
179
|
+
type: "string",
|
|
180
|
+
enum: ["min", "low", "default", "high", "urgent"],
|
|
181
|
+
description: "Optional urgency tag. Default = 'default'. The server doesn't enforce semantics — receivers (listen-here, agents, webhooks) interpret. Use 'urgent' when the peer should wake right now; 'low' or 'min' for background updates the peer can batch.",
|
|
182
|
+
},
|
|
183
|
+
suggested_replies: {
|
|
184
|
+
type: "array",
|
|
185
|
+
items: { type: "string", maxLength: 64 },
|
|
186
|
+
maxItems: 4,
|
|
187
|
+
description: "Optional array of up to 4 short canned replies (max 64 chars each). Useful when asking the peer a multiple-choice-ish question, especially in human-in-the-loop channels. E.g. send 'shall I deploy?' with suggested_replies=['yes', 'no', 'show me the diff']. The 'click' from a receiver is just a normal /send with that text.",
|
|
188
|
+
},
|
|
173
189
|
},
|
|
174
190
|
required: ["to", "message"],
|
|
175
191
|
},
|
|
@@ -316,11 +332,15 @@ async function callChannelTool(channel, sessionId, name, args) {
|
|
|
316
332
|
case "send": {
|
|
317
333
|
const to = String(args.to ?? "");
|
|
318
334
|
const message = String(args.message ?? args.text ?? "");
|
|
319
|
-
const
|
|
335
|
+
const priority = isPriority(args.priority) ? args.priority : undefined;
|
|
336
|
+
const suggestedReplies = validateSuggestedReplies(args.suggested_replies);
|
|
337
|
+
const msg = channel.send(sessionId, to, message, priority, suggestedReplies);
|
|
320
338
|
statsRecordMessage();
|
|
321
339
|
transcriptRecordMessage(channel.id, getChannelRetention(channel.id), msg);
|
|
322
340
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
323
|
-
|
|
341
|
+
const prio = msg.priority ? ` [${msg.priority}]` : "";
|
|
342
|
+
const replies = msg.suggested_replies ? ` (suggested: ${msg.suggested_replies.join(" | ")})` : "";
|
|
343
|
+
return textContent(`sent #${msg.id}${prio} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}${replies}`);
|
|
324
344
|
}
|
|
325
345
|
case "listen": {
|
|
326
346
|
const seconds = typeof args.timeout_seconds === "number" ? args.timeout_seconds : 30;
|
|
@@ -598,11 +618,15 @@ async function callUnifiedTool(name, args, state, sessionId, publicOrigin, mode
|
|
|
598
618
|
case "send": {
|
|
599
619
|
const to = String(args.to ?? "");
|
|
600
620
|
const message = String(args.message ?? args.text ?? "");
|
|
601
|
-
const
|
|
621
|
+
const priority = isPriority(args.priority) ? args.priority : undefined;
|
|
622
|
+
const suggestedReplies = validateSuggestedReplies(args.suggested_replies);
|
|
623
|
+
const msg = channel.send(sessionId, to, message, priority, suggestedReplies);
|
|
602
624
|
statsRecordMessage();
|
|
603
625
|
transcriptRecordMessage(channel.id, getChannelRetention(channel.id), msg);
|
|
604
626
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
605
|
-
|
|
627
|
+
const prio = msg.priority ? ` [${msg.priority}]` : "";
|
|
628
|
+
const replies = msg.suggested_replies ? ` (suggested: ${msg.suggested_replies.join(" | ")})` : "";
|
|
629
|
+
return textContent(`sent #${msg.id}${prio} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}${replies}`);
|
|
606
630
|
}
|
|
607
631
|
case "listen": {
|
|
608
632
|
const seconds = typeof args.timeout_seconds === "number" ? args.timeout_seconds : 30;
|
package/dist/remote-ui.js
CHANGED
|
@@ -113,6 +113,19 @@ export function remoteHtml(channelId) {
|
|
|
113
113
|
}
|
|
114
114
|
.msg.sys.error .bubble { color: var(--warn); }
|
|
115
115
|
.msg .when { font-size: 10px; color: var(--dim); padding: 0 6px; }
|
|
116
|
+
.msg .chips {
|
|
117
|
+
display: flex; flex-wrap: wrap; gap: 6px;
|
|
118
|
+
margin-top: 4px; padding: 0 4px;
|
|
119
|
+
}
|
|
120
|
+
.msg .chip {
|
|
121
|
+
font-family: inherit; font-size: 13px;
|
|
122
|
+
padding: 6px 12px;
|
|
123
|
+
background: var(--paper); color: var(--ink);
|
|
124
|
+
border: 1px solid var(--warn); border-radius: 14px;
|
|
125
|
+
cursor: pointer; touch-action: manipulation;
|
|
126
|
+
transition: background 0.1s;
|
|
127
|
+
}
|
|
128
|
+
.msg .chip:active { background: var(--warn); color: white; }
|
|
116
129
|
|
|
117
130
|
footer {
|
|
118
131
|
flex-shrink: 0; background: var(--paper);
|
|
@@ -315,6 +328,29 @@ export function remoteHtml(channelId) {
|
|
|
315
328
|
var when = document.createElement('div'); when.className = 'when';
|
|
316
329
|
when.textContent = fmtTime(m.at);
|
|
317
330
|
d.appendChild(who); d.appendChild(b); d.appendChild(when);
|
|
331
|
+
// Suggested replies → render as tappable chips below the bubble (only on
|
|
332
|
+
// incoming msgs; if I sent it, no point clicking my own suggestion).
|
|
333
|
+
if (!mine && m.suggested_replies && m.suggested_replies.length){
|
|
334
|
+
var chipBar = document.createElement('div');
|
|
335
|
+
chipBar.className = 'chips';
|
|
336
|
+
for (var i = 0; i < m.suggested_replies.length; i++){
|
|
337
|
+
var chip = document.createElement('button');
|
|
338
|
+
chip.type = 'button';
|
|
339
|
+
chip.className = 'chip';
|
|
340
|
+
chip.textContent = m.suggested_replies[i];
|
|
341
|
+
// Capture in closure-friendly way for older mobile JS engines
|
|
342
|
+
chip.addEventListener('click', (function(text, fromCallsign){
|
|
343
|
+
return function(){
|
|
344
|
+
elInput.value = text;
|
|
345
|
+
elTarget.value = fromCallsign; // reply to whoever sent the suggestion
|
|
346
|
+
autosize();
|
|
347
|
+
send();
|
|
348
|
+
};
|
|
349
|
+
})(m.suggested_replies[i], m.from));
|
|
350
|
+
chipBar.appendChild(chip);
|
|
351
|
+
}
|
|
352
|
+
d.appendChild(chipBar);
|
|
353
|
+
}
|
|
318
354
|
elLog.appendChild(d);
|
|
319
355
|
scrollDown();
|
|
320
356
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rogerrat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"mcpName": "io.github.opcastil11/rogerrat",
|
|
5
5
|
"description": "Real-time chat for AI agents. A walkie-talkie hub that lets two or more agents — Claude Code, Cursor, Cline, Claude Desktop, Codex — on different machines send messages to each other over MCP or plain REST. Hosted at rogerrat.chat or self-hosted with `npx rogerrat`.",
|
|
6
6
|
"keywords": [
|