viberoom 0.3.0 → 0.3.1

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/persona.js CHANGED
@@ -23,7 +23,7 @@ export const DEFAULT_ROOM_SETTINGS = {
23
23
  humanDescriptionMode: "inherit",
24
24
  refereeAction: "next-header",
25
25
  turnTaking: "parallel",
26
- replyDelay: 5,
26
+ replyDelay: 4,
27
27
  waitWhileHumanTypes: true,
28
28
  };
29
29
  export const BRIEF_AFFECTING_SETTINGS = [
package/dist/server.js CHANGED
@@ -3,6 +3,8 @@ import { spawn } from "node:child_process";
3
3
  import { createServer } from "node:http";
4
4
  import { readFile, stat } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import { dirname, join } from "node:path";
6
8
  import { existsSync as fileExists } from "node:fs";
7
9
  import { classifyOpenTarget, describeOpen, detectEditor, editorCommand, isExecutablePath, openCommand } from "./open.js";
8
10
  import { parseCsv, viewerKind, VIEWER_MAX_BYTES } from "./viewer.js";
@@ -38,8 +40,14 @@ const STATIC_FILES = {
38
40
  export function startServer(hub, port, log, info, onShutdownRequest) {
39
41
  const uiDir = fileURLToPath(new URL("../ui/", import.meta.url));
40
42
  const assetsDir = fileURLToPath(new URL("../assets/", import.meta.url));
41
- const modulesDir = fileURLToPath(new URL("../node_modules/", import.meta.url));
42
- const staticDir = (dir) => (dir === "assets" ? assetsDir : dir === "node_modules" ? modulesDir : uiDir);
43
+ const resolveModule = createRequire(import.meta.url).resolve;
44
+ const packageDir = (name) => dirname(resolveModule(`${name}/package.json`));
45
+ const staticPath = (entry) => {
46
+ if (entry.dir !== "node_modules")
47
+ return (entry.dir === "assets" ? assetsDir : uiDir) + entry.file;
48
+ const slash = entry.file.indexOf("/");
49
+ return join(packageDir(entry.file.slice(0, slash)), entry.file.slice(slash + 1));
50
+ };
43
51
  const clients = new Set();
44
52
  const snapshot = () => ({ ...hub.snapshot(), version: info });
45
53
  const broadcast = (event) => {
@@ -82,7 +90,7 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
82
90
  }
83
91
  if (req.method === "GET" && STATIC_FILES[path]) {
84
92
  const entry = STATIC_FILES[path];
85
- const body = await readFile(staticDir(entry.dir) + entry.file);
93
+ const body = await readFile(staticPath(entry));
86
94
  res.writeHead(200, { "Content-Type": entry.type, "Cache-Control": entry.dir === "ui" || !entry.dir ? "no-cache" : "public, max-age=3600" });
87
95
  res.end(body);
88
96
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberoom",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "viberoom: group chat rooms for a human and several coding agents over the Agent Client Protocol",
5
5
  "type": "module",
6
6
  "engines": {
package/ui/app.js CHANGED
@@ -935,21 +935,21 @@
935
935
  els.sideRoomName.textContent = room.name;
936
936
  els.sideRoomEmoji.textContent = room.settings.emoji || "";
937
937
  els.sideRoomSub.textContent = room.settings.topic || `${st.agents.length} vibemate${st.agents.length === 1 ? "" : "s"}${st.agents.length ? ` · ${st.online} online` : ""}`;
938
- els.participants.innerHTML = "";
939
938
  const ordered = [...room.participants].sort((a, b) => (a.kind === "human" ? -1 : b.kind === "human" ? 1 : 0));
939
+ const rows = new Map([...els.participants.children].map((li) => [li.dataset.id, li]));
940
940
  for (const p of ordered) {
941
- const li = document.createElement("li");
941
+ let li = rows.get(p.id);
942
942
  const selected = (state.selection.kind === "participant" && state.selection.id === p.id) || (p.kind === "human" && state.selection.kind === "me" && state.detailsOpen);
943
943
  const asleep = p.kind === "agent" && (p.status === "offline" || p.status === "left");
944
- li.className = (p.kind === "human" ? "me" : "") + (selected ? " selected" : "") + (asleep ? " offline" : "");
944
+ const className = (p.kind === "human" ? "me" : "") + (selected ? " selected" : "") + (asleep ? " offline" : "");
945
945
  const sub = p.kind === "human" ? "you, the human" : [p.tagline ? `"${p.tagline}"` : "", p.agentVendor || p.agentLabel, p.model].filter(Boolean).join(" · ");
946
946
  const warn = p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<div class="p-warn" title="${esc(p.statusDetail)}">${esc(p.statusDetail)}</div>` : "";
947
947
  const status = asleep
948
948
  ? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
949
949
  : p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${p.status}">${p.status === "thinking" ? '<span class="dot"></span>' : ""}${STATUS_LABEL[p.status] || p.status}</span>` : "";
950
- li.innerHTML = `
951
- ${avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true, status: p.kind === "agent" })}
952
- <div class="p-body">
950
+ const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true });
951
+ const statusClass = p.kind === "agent" ? `avatar-status status-${esc(p.status || "idle")}` : "";
952
+ const bodyHtml = `<div class="p-body">
953
953
  <div class="p-name"><span>${esc(p.name)}</span>${p.muted ? '<span class="badge muted">muted</span>' : ""}${status}</div>
954
954
  <div class="p-sub">${esc(sub)}</div>
955
955
  ${warn}
@@ -958,20 +958,32 @@
958
958
  ${p.kind === "agent" && p.status === "thinking" ? `<button class="icon-btn sm stop-btn" title="Stop this reply">${ic("stop")}</button>` : ""}
959
959
  ${p.kind === "agent" && p.status === "offline" ? `<button class="icon-btn sm reconnect-btn" title="Reconnect">${ic("refresh")}</button>` : ""}
960
960
  </div>`;
961
- li.addEventListener("click", (e) => {
962
- if (e.target.closest("button")) return;
963
- if (p.kind === "human") openDetails({ kind: "me" });
964
- else openDetails({ kind: "participant", id: p.id });
965
- });
966
- li.addEventListener("dblclick", () => {
967
- if (p.kind === "agent") insertMention(p.name);
968
- });
969
- const stop = li.querySelector(".stop-btn");
970
- if (stop) stop.addEventListener("click", () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/cancel`)).catch(showError));
971
- const rec = li.querySelector(".reconnect-btn");
972
- if (rec) rec.addEventListener("click", () => openReconnectDialog(room));
973
- els.participants.appendChild(li);
961
+ if (!li) {
962
+ li = document.createElement("li");
963
+ li.dataset.id = p.id;
964
+ li.innerHTML = avatarHtml + bodyHtml;
965
+ li.dataset.avatar = avatarHtml;
966
+ if (statusClass) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="${statusClass}"></span>`);
967
+ li.dataset.body = bodyHtml;
968
+ } else {
969
+ if (li.dataset.avatar !== avatarHtml) {
970
+ li.querySelector(".avatar").outerHTML = avatarHtml;
971
+ li.dataset.avatar = avatarHtml;
972
+ }
973
+ const dot = li.querySelector(".avatar-status");
974
+ if (statusClass && dot && dot.className !== statusClass) dot.className = statusClass;
975
+ else if (statusClass && !dot) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="${statusClass}"></span>`);
976
+ if (li.dataset.body !== bodyHtml) {
977
+ li.querySelectorAll(".p-body, .p-actions").forEach((el) => el.remove());
978
+ li.insertAdjacentHTML("beforeend", bodyHtml);
979
+ li.dataset.body = bodyHtml;
980
+ }
981
+ }
982
+ if (li.className !== className) li.className = className;
983
+ if (li !== els.participants.children[ordered.indexOf(p)]) els.participants.appendChild(li);
984
+ rows.delete(p.id);
974
985
  }
986
+ for (const li of rows.values()) li.remove();
975
987
  renderHushButton(room);
976
988
  els.reconnectAllBtn.hidden = offlineAgents(room).length === 0;
977
989
  }
@@ -1675,7 +1687,7 @@
1675
1687
  <div class="section">
1676
1688
  ${sectionTitle("user", "Turn taking")}
1677
1689
  ${field("Who may speak", `<select id="rp-turns"><option value="one-at-a-time"${rs.turnTaking !== "parallel" ? " selected" : ""}>One vibemate at a time</option><option value="parallel"${rs.turnTaking === "parallel" ? " selected" : ""}>All addressed vibemates at once</option></select>`, null, "One at a time: the others queue and see the earlier replies before they answer; the addressed vibemates go first. All at once: fastest, but replies may cross.")}
1678
- ${field("Reply delay, seconds", `<input type="number" id="rp-delay" min="0" max="120" step="0.5" value="${rs.replyDelay ?? 5}">`, "With two or more vibemates, each waits a random 0–N seconds before it answers, so replies cross less often. A vibemate alone answers at once. A vibemate's own delay (in its panel) always applies.")}
1690
+ ${field("Reply delay, seconds", `<input type="number" id="rp-delay" min="0" max="120" step="0.5" value="${rs.replyDelay ?? 4}">`, "With two or more vibemates, each waits a random 0–N seconds before it answers, so replies cross less often. A vibemate alone answers at once. A vibemate's own delay (in its panel) always applies.")}
1679
1691
  <label class="switch"><span class="label">Wait while you are typing${geekTip("A vibemate about to start holds back while you type (a few seconds after your last keystroke). A reply already under way is not interrupted.")}</span><input type="checkbox" id="rp-wait-typing" ${rs.waitWhileHumanTypes !== false ? "checked" : ""}></label>
1680
1692
  </div>
1681
1693
  ${geek(
@@ -1794,7 +1806,7 @@
1794
1806
  <div class="section">
1795
1807
  ${sectionTitle("bolt", "Pace")}
1796
1808
  ${field("Turn taking in new rooms", `<select id="sp-turns"><option value="one-at-a-time"${d.turnTaking !== "parallel" ? " selected" : ""}>One vibemate at a time</option><option value="parallel"${d.turnTaking === "parallel" ? " selected" : ""}>All addressed vibemates at once</option></select>`)}
1797
- ${field("Reply delay in new rooms, seconds", `<input type="number" id="sp-delay" min="0" max="120" step="0.5" value="${d.replyDelay ?? 5}">`, "Used when two or more vibemates share a room; each room can change it; a vibemate can override it in its own panel.", "Before each turn a vibemate waits a random 0–N seconds, so replies cross less often. Messages that arrive meanwhile land in its backlog. A vibemate alone answers at once unless it has its own delay.")}
1809
+ ${field("Reply delay in new rooms, seconds", `<input type="number" id="sp-delay" min="0" max="120" step="0.5" value="${d.replyDelay ?? 4}">`, "Used when two or more vibemates share a room; each room can change it; a vibemate can override it in its own panel.", "Before each turn a vibemate waits a random 0–N seconds, so replies cross less often. Messages that arrive meanwhile land in its backlog. A vibemate alone answers at once unless it has its own delay.")}
1798
1810
  </div>
1799
1811
  <div class="section" id="sp-editor">
1800
1812
  ${sectionTitle("pencil", "Open files at a line")}
@@ -2305,7 +2317,7 @@
2305
2317
  els.invNote.textContent = "";
2306
2318
  els.invStatus.textContent = "";
2307
2319
  els.invDelay.value = "";
2308
- els.invDelay.placeholder = `the room's: ${(currentRoom() || {}).settings?.replyDelay ?? 5} s`;
2320
+ els.invDelay.placeholder = `the room's: ${(currentRoom() || {}).settings?.replyDelay ?? 4} s`;
2309
2321
  renderSkillChecks(els.invSkills, []);
2310
2322
  els.invName.value = "";
2311
2323
  els.invAvatar.value = "";
@@ -2708,20 +2720,53 @@
2708
2720
  typingSentAt = now;
2709
2721
  post(roomApi("/typing"), {}).catch(() => undefined);
2710
2722
  });
2723
+ els.participants.addEventListener("click", (e) => {
2724
+ const li = e.target.closest("li[data-id]");
2725
+ const room = currentRoom();
2726
+ if (!li || !room) return;
2727
+ const p = findById(room, li.dataset.id);
2728
+ if (!p) return;
2729
+ if (e.target.closest(".stop-btn")) return void post(roomApi(`/participants/${encodeURIComponent(p.id)}/cancel`)).catch(showError);
2730
+ if (e.target.closest(".reconnect-btn")) return openReconnectDialog(room);
2731
+ if (e.target.closest("button")) return;
2732
+ if (p.kind === "human") openDetails({ kind: "me" });
2733
+ else openDetails({ kind: "participant", id: p.id });
2734
+ });
2735
+ els.participants.addEventListener("dblclick", (e) => {
2736
+ const li = e.target.closest("li[data-id]");
2737
+ const p = li && findById(currentRoom(), li.dataset.id);
2738
+ if (p && p.kind === "agent") insertMention(p.name);
2739
+ });
2711
2740
  els.composer.addEventListener("submit", async (event) => {
2712
2741
  event.preventDefault();
2713
2742
  const text = els.input.value.trim();
2714
- if (!text || !currentRoom()) return;
2743
+ const room = currentRoom();
2744
+ if (!text || !room) return;
2715
2745
  els.input.value = "";
2716
2746
  typingSentAt = 0;
2717
2747
  autosize();
2748
+ const local = { id: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, seq: 0, from: "human", fromName: (state.settings || {}).humanName || "You", to: [], toNames: [], text, ts: Date.now(), kind: "chat", pending: true };
2749
+ upsertMessage(room.id, local);
2718
2750
  try {
2719
- await post(roomApi("/send"), { text });
2751
+ const r = await post(roomApi("/send"), { text });
2752
+ adoptLocalMessage(room.id, local.id, r.id);
2720
2753
  } catch (error) {
2754
+ removeMessage(room.id, local.id);
2721
2755
  showError(error);
2722
2756
  els.input.value = text;
2723
2757
  }
2724
2758
  });
2759
+ function adoptLocalMessage(roomId, localId, realId) {
2760
+ const room = state.rooms.get(roomId);
2761
+ if (!room || !realId) return;
2762
+ if (room.messages.some((m) => m.id === realId)) return removeMessage(roomId, localId);
2763
+ const m = room.messages.find((x) => x.id === localId);
2764
+ if (!m) return;
2765
+ m.id = realId;
2766
+ delete m.pending;
2767
+ const el = els.messages.querySelector(`.msg[data-id="${localId}"]`);
2768
+ if (el) el.dataset.id = realId;
2769
+ }
2725
2770
 
2726
2771
 
2727
2772
  const RULE_MENTION_RE = /(?<![\w.\/:])@([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;