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 +1 -1
- package/dist/server.js +11 -3
- package/package.json +1 -1
- package/ui/app.js +69 -24
package/dist/persona.js
CHANGED
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
|
|
42
|
-
const
|
|
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(
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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;
|