@sparkelf/dsh-group-chat 0.1.1 → 0.2.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/lib/index.js CHANGED
@@ -345,6 +345,17 @@ var GroupChatBackend = class {
345
345
  * Pull events after `cursor`. Omitting the cursor asks for the room from the start,
346
346
  * which is what a fresh container does on its first sync.
347
347
  */
348
+ /**
349
+ * Searches message text across the rooms this account can read.
350
+ *
351
+ * The visibility rule lives on the room server: it decides which rooms the caller may read and
352
+ * narrows the query there. Forwarding the words alone is deliberate — a copy of that rule in
353
+ * the plugin would be a second place to get authorization wrong.
354
+ */
355
+ async searchMessages(query, signal) {
356
+ const params = new URLSearchParams({ query });
357
+ return this.request("GET", `/api/ai/chat/rooms/messages/search?${params.toString()}`, void 0, signal);
358
+ }
348
359
  async listEvents(roomId, cursor, limit, signal) {
349
360
  const query = new URLSearchParams();
350
361
  if (cursor !== void 0) query.set("cursor", cursor);
@@ -8869,6 +8880,14 @@ const name = "group-chat";
8869
8880
  * everyone. Recognized by id because that survives the round trip through the session.
8870
8881
  */
8871
8882
  const WARM_UP_PREFIX = "warm-up-";
8883
+ /**
8884
+ * The block type a reply uses to carry the message it answers.
8885
+ *
8886
+ * The host bundle and the client bundle are evaluated separately, so this string is written
8887
+ * here as well as in the renderer that reads it. `quote-block-type.test.ts` holds the two
8888
+ * together: a silent divergence would publish quotes nothing draws.
8889
+ */
8890
+ const QUOTE_BLOCK_TYPE = "group-chat-quote";
8872
8891
  /** Host services this plugin needs beyond the room runtime. */
8873
8892
  const inject = [
8874
8893
  "agents",
@@ -9131,6 +9150,17 @@ const activate = (ctx, target) => {
9131
9150
  }),
9132
9151
  onError: (error) => log?.error?.("[group-chat] stream unavailable; polling continues", error)
9133
9152
  });
9153
+ /**
9154
+ * The reply each room is waiting to send, keyed by room id.
9155
+ *
9156
+ * A quote cannot be put into the message by the browser: a room message is typed into the
9157
+ * official composer, and this plugin never sees or rewrites what that composer submits — it
9158
+ * only observes the session event afterwards. So the browser registers the quote here, and the
9159
+ * publish path attaches it to the next message that member sends. The entry is consumed on
9160
+ * first use: a quote belongs to one reply, and leaving it behind would attach it to every
9161
+ * later message too.
9162
+ */
9163
+ const pendingQuotes = /* @__PURE__ */ new Map();
9134
9164
  const runtimes = /* @__PURE__ */ new Map();
9135
9165
  const timers = /* @__PURE__ */ new Set();
9136
9166
  /**
@@ -9364,8 +9394,16 @@ const activate = (ctx, target) => {
9364
9394
  if (typeof messageId === "string" && messageId.startsWith(WARM_UP_PREFIX)) return;
9365
9395
  const content = data.content;
9366
9396
  const mentions = mentionedMemberIds(textOfContent(content), runtimes.get(roomId)?.members ?? []);
9397
+ const quote = pendingQuotes.get(roomId);
9398
+ pendingQuotes.delete(roomId);
9399
+ const outgoing = quote === void 0 ? content : [...content, {
9400
+ type: QUOTE_BLOCK_TYPE,
9401
+ messageId: quote.messageId,
9402
+ preview: quote.preview,
9403
+ ...quote.authorName === void 0 ? {} : { authorName: quote.authorName }
9404
+ }];
9367
9405
  backend.sendMessage(roomId, {
9368
- content,
9406
+ content: outgoing,
9369
9407
  rpcId: typeof messageId === "string" && messageId.length > 0 ? messageId : `local-${String(Date.now())}`,
9370
9408
  ...mentions.length === 0 ? {} : { mentions }
9371
9409
  }).catch((error) => {
@@ -9414,7 +9452,7 @@ const activate = (ctx, target) => {
9414
9452
  const route = url.pathname.slice(19).replace(/^\/+/u, "");
9415
9453
  try {
9416
9454
  const payload = await readJsonBody(request);
9417
- const body = await handleRoomRequest(route, request.method ?? "GET", payload, url, backend, runtimes, closeRoom, ctx);
9455
+ const body = await handleRoomRequest(route, request.method ?? "GET", payload, url, backend, runtimes, closeRoom, ctx, pendingQuotes);
9418
9456
  response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
9419
9457
  response.end(JSON.stringify(body ?? null));
9420
9458
  } catch (error) {
@@ -9476,9 +9514,14 @@ const activate = (ctx, target) => {
9476
9514
  * requests to the DSH host, so the routes are same-origin and need no separate
9477
9515
  * transport or credential.
9478
9516
  */
9479
- const handleRoomRequest = async (route, httpMethod, payload, url, backend, runtimes, closeRoom, ctx) => {
9517
+ const handleRoomRequest = async (route, httpMethod, payload, url, backend, runtimes, closeRoom, ctx, pendingQuotes) => {
9480
9518
  const [resource, roomId, ...rest] = route.split("/").filter((part) => part.length > 0);
9481
9519
  const action = rest.join("/");
9520
+ if (resource === "rooms" && roomId === "messages" && action === "search") {
9521
+ if (httpMethod !== "GET") throw new RouteRefusal("message search accepts GET", 405);
9522
+ const query = url.searchParams.get("query") ?? "";
9523
+ return backend.searchMessages(query);
9524
+ }
9482
9525
  if (resource === "preferences") {
9483
9526
  if (httpMethod === "GET") return readPreferences();
9484
9527
  if (httpMethod === "PATCH" || httpMethod === "POST") {
@@ -9534,6 +9577,26 @@ const handleRoomRequest = async (route, httpMethod, payload, url, backend, runti
9534
9577
  }
9535
9578
  throw new Error(`unknown group-chat request: ${httpMethod} ${route}`);
9536
9579
  }
9580
+ if (action === "quote") {
9581
+ if (httpMethod === "POST") {
9582
+ const messageId = payload?.["messageId"];
9583
+ const preview = payload?.["preview"];
9584
+ const authorName = payload?.["authorName"];
9585
+ if (typeof messageId !== "string" || messageId.length === 0) throw new RouteRefusal("a quote needs the message it replies to", 400);
9586
+ if (typeof preview !== "string") throw new RouteRefusal("a quote needs a preview string", 400);
9587
+ pendingQuotes.set(roomId, {
9588
+ messageId,
9589
+ preview,
9590
+ ...typeof authorName === "string" && authorName.length > 0 ? { authorName } : {}
9591
+ });
9592
+ return { ok: true };
9593
+ }
9594
+ if (httpMethod === "DELETE") {
9595
+ pendingQuotes.delete(roomId);
9596
+ return { ok: true };
9597
+ }
9598
+ throw new RouteRefusal("a quote accepts POST and DELETE", 405);
9599
+ }
9537
9600
  if (action === "join" && httpMethod === "POST") return backend.joinRoom(roomId);
9538
9601
  if (httpMethod === "GET") {
9539
9602
  if (action === "events") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkelf/dsh-group-chat",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Human-agent group chat for DataOps-managed DSH workspaces: rooms broadcast to every member, and only a member's own @mention wakes that member's agent.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",