viberoom 0.5.2 → 0.5.4
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/NOTICE +8 -2
- package/README.md +1 -1
- package/dist/hub.js +55 -2
- package/dist/main.js +10 -0
- package/dist/room.js +43 -3
- package/dist/server.js +41 -1
- package/dist/templates.js +27 -3
- package/dist/update.js +95 -0
- package/package.json +1 -1
- package/templates/wren-and-quinn/template.json +28 -0
- package/ui/app.css +61 -19
- package/ui/app.js +306 -14
- package/ui/fonts/fira-code-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/fira-code-cyrillic.woff2 +0 -0
- package/ui/fonts/fira-code-latin-ext.woff2 +0 -0
- package/ui/fonts/fira-code-latin.woff2 +0 -0
- package/ui/fonts/fira-code.css +5 -0
- package/ui/fonts/inter-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/inter-cyrillic.woff2 +0 -0
- package/ui/fonts/inter-latin-ext.woff2 +0 -0
- package/ui/fonts/inter-latin.woff2 +0 -0
- package/ui/fonts/inter.css +5 -0
- package/ui/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/ui/fonts/jetbrains-mono.css +5 -0
- package/ui/fonts/noto-sans-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/noto-sans-cyrillic.woff2 +0 -0
- package/ui/fonts/noto-sans-latin-ext.woff2 +0 -0
- package/ui/fonts/noto-sans-latin.woff2 +0 -0
- package/ui/fonts/noto-sans.css +5 -0
- package/ui/fonts/source-code-pro-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/source-code-pro-cyrillic.woff2 +0 -0
- package/ui/fonts/source-code-pro-latin-ext.woff2 +0 -0
- package/ui/fonts/source-code-pro-latin.woff2 +0 -0
- package/ui/fonts/source-code-pro.css +5 -0
- package/ui/index.html +11 -0
- package/ui/theme.css +44 -18
- package/templates/forge-and-lumen/template.json +0 -28
package/ui/app.js
CHANGED
|
@@ -128,7 +128,39 @@
|
|
|
128
128
|
eraseSubmit: $("#erase-submit"),
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
-
const
|
|
131
|
+
const FONTS = {
|
|
132
|
+
text: {
|
|
133
|
+
nunito: { label: "Nunito (default)", stack: '"Nunito", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
134
|
+
inter: { label: "Inter", stack: '"Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
135
|
+
"noto-sans": { label: "Noto Sans", stack: '"Noto Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
136
|
+
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
137
|
+
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
138
|
+
},
|
|
139
|
+
mono: {
|
|
140
|
+
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
141
|
+
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
142
|
+
"source-code-pro": { label: "Source Code Pro", stack: '"Source Code Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
143
|
+
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const STATUS_LABEL = { unstaffed: "needs a coding agent", starting: "starting…", idle: "ready", queued: "waiting…", thinking: "thinking…", writing: "writing…", error: "error", offline: "offline", left: "left" };
|
|
147
|
+
const FALLBACK_COLOR = "#9ca3af";
|
|
148
|
+
const WORKING_SVG = '<svg class="working" viewBox="0 0 44 35" aria-hidden="true" title="working…">'
|
|
149
|
+
+ '<g class="body">'
|
|
150
|
+
+ '<circle cx="20" cy="6.5" r="5.6" fill="currentColor"/>'
|
|
151
|
+
+ '<path d="M6 35L13.7 16.5a4 4 0 0 1 8 0L14 35z" fill="currentColor"/>'
|
|
152
|
+
+ '</g>'
|
|
153
|
+
+ '<path d="M19 19.5L23 27" fill="none" stroke="currentColor" stroke-width="4.4" stroke-linecap="round"/>'
|
|
154
|
+
+ '<g class="forearm"><path d="M23 27h10.5" fill="none" stroke="currentColor" stroke-width="4.4" stroke-linecap="round"/></g>'
|
|
155
|
+
+ '<rect x="26" y="30.2" width="15.5" height="3" rx="1" fill="currentColor"/>'
|
|
156
|
+
+ '<path d="M35.9 30.2L41.1 14h2.4l-5.1 16.2z" fill="currentColor"/>'
|
|
157
|
+
+ '<rect x="18" y="33.2" width="25" height="1.3" rx=".6" fill="currentColor"/>'
|
|
158
|
+
+ '</svg>';
|
|
159
|
+
|
|
160
|
+
function shownStatus(room, p) {
|
|
161
|
+
if (p.status !== "thinking") return p.status;
|
|
162
|
+
return room.messages.some((m) => m.streaming && m.from === p.id) ? "writing" : "thinking";
|
|
163
|
+
}
|
|
132
164
|
const CHAT_EMOJI = ["😀", "😄", "😂", "🙂", "😉", "😍", "🤔", "😎", "🥳", "😅", "😢", "😡", "👍", "👎", "👋", "🙏", "👏", "💪", "🔥", "✨", "🎉", "❤️", "💜", "✅", "❌", "⚠️", "💡", "🚀", "🐛", "🤖", "🤫", "☕"];
|
|
133
165
|
const ROOM_EMOJI = ["🎭", "🚀", "🧪", "🛠️", "🎨", "📚", "🧠", "💬", "🔬", "🎯", "🐙", "☕", "🌈", "🏗️", "🎮", "🔥", "🧩", "📈", "🗺️", "🎧", "🌱", "🏠", "🛸", "🧭"];
|
|
134
166
|
function emojiGrid(list, current, onPick) {
|
|
@@ -391,7 +423,7 @@
|
|
|
391
423
|
function mermaidThemeVariables(d) {
|
|
392
424
|
d = diagramSettings(d);
|
|
393
425
|
const preset = DIAGRAM_PRESETS[d.preset] || DIAGRAM_PRESETS.pop;
|
|
394
|
-
const vars = Object.assign({}, preset, { fontFamily: "
|
|
426
|
+
const vars = Object.assign({}, preset, { fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font").trim() || "Nunito, sans-serif", fontSize: "13px" });
|
|
395
427
|
delete vars.label;
|
|
396
428
|
delete vars.palette;
|
|
397
429
|
if (preset.palette) preset.palette.forEach((c, i) => (vars[`pie${i + 1}`] = c.fill));
|
|
@@ -791,6 +823,51 @@
|
|
|
791
823
|
maybeOfferReconnect();
|
|
792
824
|
}
|
|
793
825
|
|
|
826
|
+
function renderUpdatePop() {
|
|
827
|
+
const old = $("#update-pop");
|
|
828
|
+
const u = state.update;
|
|
829
|
+
const show = u && u.available && u.latest && recall("updateDismissed") !== u.latest;
|
|
830
|
+
if (!show) {
|
|
831
|
+
if (old && !old.dataset.busy) old.remove();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (old && old.dataset.version === u.latest) return;
|
|
835
|
+
if (old) old.remove();
|
|
836
|
+
const pop = document.createElement("div");
|
|
837
|
+
pop.id = "update-pop";
|
|
838
|
+
pop.className = "update-pop";
|
|
839
|
+
pop.dataset.version = u.latest;
|
|
840
|
+
pop.innerHTML = `<div class="up-main"><div class="up-text"><b>viberoom ${esc(u.latest)}</b> is out. You have ${esc(u.current)}.</div><button type="button" class="btn sm primary up-go">Update now and restart</button></div>
|
|
841
|
+
<div class="up-side"><button type="button" class="icon-btn sm up-x" title="Not now">${ic("close")}</button><button type="button" class="icon-btn sm up-settings" title="Update settings">${ic("settings")}</button></div>`;
|
|
842
|
+
pop.querySelector(".up-x").addEventListener("click", () => {
|
|
843
|
+
remember("updateDismissed", u.latest);
|
|
844
|
+
pop.remove();
|
|
845
|
+
});
|
|
846
|
+
pop.querySelector(".up-settings").addEventListener("click", () => setView("settings"));
|
|
847
|
+
pop.querySelector(".up-go").addEventListener("click", () => installUpdate(pop, u.latest));
|
|
848
|
+
els.rail.querySelector(".rail-foot").appendChild(pop);
|
|
849
|
+
}
|
|
850
|
+
async function installUpdate(pop, version) {
|
|
851
|
+
const go = pop.querySelector(".up-go");
|
|
852
|
+
const text = pop.querySelector(".up-text");
|
|
853
|
+
pop.dataset.busy = "1";
|
|
854
|
+
go.disabled = true;
|
|
855
|
+
go.classList.add("loading");
|
|
856
|
+
text.innerHTML = `Installing <b>viberoom ${esc(version)}</b>… this takes a moment.`;
|
|
857
|
+
try {
|
|
858
|
+
await post("/api/update/install", {});
|
|
859
|
+
go.classList.remove("loading");
|
|
860
|
+
text.innerHTML = `<b>viberoom ${esc(version)}</b> is installed. Restarting…`;
|
|
861
|
+
go.hidden = true;
|
|
862
|
+
} catch (e) {
|
|
863
|
+
delete pop.dataset.busy;
|
|
864
|
+
go.classList.remove("loading");
|
|
865
|
+
go.disabled = false;
|
|
866
|
+
go.textContent = "Try again";
|
|
867
|
+
text.innerHTML = `<span class="error">${esc(e.message || String(e))}</span>`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
794
871
|
function renderRail() {
|
|
795
872
|
els.rail.querySelectorAll(".rail-item[data-nav]").forEach((b) => {
|
|
796
873
|
const nav = b.dataset.nav;
|
|
@@ -1070,6 +1147,7 @@
|
|
|
1070
1147
|
const selected = (state.selection.kind === "participant" && state.selection.id === p.id) || (p.kind === "human" && state.selection.kind === "me" && state.detailsOpen);
|
|
1071
1148
|
const asleep = p.kind === "agent" && (p.status === "offline" || p.status === "left");
|
|
1072
1149
|
const unstaffed = p.kind === "agent" && p.status === "unstaffed";
|
|
1150
|
+
const shown = shownStatus(room, p);
|
|
1073
1151
|
const className = (p.kind === "human" ? "me" : "") + (selected ? " selected" : "") + (asleep ? " offline" : "") + (unstaffed ? " unstaffed" : "");
|
|
1074
1152
|
const sub = p.kind === "human" ? "you, the human" : [p.tagline ? `"${p.tagline}"` : "", p.agentVendor || p.agentLabel, p.model].filter(Boolean).join(" · ");
|
|
1075
1153
|
const warn = p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<div class="p-warn" title="${esc(p.statusDetail)}">${esc(p.statusDetail)}</div>` : "";
|
|
@@ -1077,9 +1155,9 @@
|
|
|
1077
1155
|
? `<span class="badge status-unstaffed" title="Click to summon this vibemate: pick the coding agent that runs it">summon</span>`
|
|
1078
1156
|
: asleep
|
|
1079
1157
|
? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
|
|
1080
|
-
: p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${
|
|
1158
|
+
: p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${shown}">${p.status === "thinking" ? '<span class="dot"></span>' : ""}${STATUS_LABEL[shown] || shown}</span>` : "";
|
|
1081
1159
|
const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true });
|
|
1082
|
-
const statusClass = p.kind === "agent" ? `avatar-status status-${esc(
|
|
1160
|
+
const statusClass = p.kind === "agent" ? `avatar-status status-${esc(shown || "idle")}` : "";
|
|
1083
1161
|
const bodyHtml = `<div class="p-body">
|
|
1084
1162
|
<div class="p-name"><span>${esc(p.name)}</span>${p.muted ? '<span class="badge muted">muted</span>' : ""}${status}</div>
|
|
1085
1163
|
<div class="p-sub">${esc(sub)}</div>
|
|
@@ -1228,7 +1306,7 @@
|
|
|
1228
1306
|
: `<div class="sys${warn ? " warn" : ""}" title="${esc(fullTime(m.ts))}">${esc(m.text)}</div>`;
|
|
1229
1307
|
return el;
|
|
1230
1308
|
}
|
|
1231
|
-
const p = findById(room, m.from) || { name: m.fromName, color:
|
|
1309
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: m.from === "human" ? "human" : "agent" };
|
|
1232
1310
|
const mine = m.from === "human";
|
|
1233
1311
|
el.className = "msg " + (mine ? "mine" : "agent");
|
|
1234
1312
|
el.innerHTML = `
|
|
@@ -1377,9 +1455,9 @@
|
|
|
1377
1455
|
const more = el.querySelector(".more");
|
|
1378
1456
|
const long = !m.streaming && m.text.length > CLAMP_CHARS;
|
|
1379
1457
|
const expanded = state.expanded.has(m.id);
|
|
1380
|
-
text.innerHTML = renderText(room, m.text, m.images) + (m.streaming ?
|
|
1458
|
+
text.innerHTML = renderText(room, m.text, m.images) + (m.streaming ? WORKING_SVG : "");
|
|
1381
1459
|
if (!m.streaming) renderDiagrams(text);
|
|
1382
|
-
if (m.streaming && !m.text) text.innerHTML = '<span class="pending"
|
|
1460
|
+
if (m.streaming && !m.text) text.innerHTML = '<span class="pending" title="thinking…"><i></i><i></i><i></i></span>';
|
|
1383
1461
|
text.classList.toggle("clamped", long && !expanded);
|
|
1384
1462
|
more.hidden = !long;
|
|
1385
1463
|
more.textContent = expanded ? "Show less" : "Show more";
|
|
@@ -1434,8 +1512,9 @@
|
|
|
1434
1512
|
if (!present.length) return "";
|
|
1435
1513
|
const seen = present.filter((p) => p.lastSeenSeq != null && p.lastSeenSeq >= m.seq);
|
|
1436
1514
|
if (seen.length === present.length) return "";
|
|
1437
|
-
|
|
1438
|
-
return `<span class="ticks"
|
|
1515
|
+
const you = m.from !== "human";
|
|
1516
|
+
if (!seen.length) return `<span class="ticks">✓</span> sent${you ? " · seen by you" : ""}`;
|
|
1517
|
+
return `<span class="ticks">✓✓</span> seen by ${esc([...(you ? ["you"] : []), ...seen.map((p) => p.name)].join(", "))}`;
|
|
1439
1518
|
}
|
|
1440
1519
|
function fillSeen(el, room, m) {
|
|
1441
1520
|
if (m.kind !== "chat" || m.streaming) return;
|
|
@@ -1561,6 +1640,7 @@
|
|
|
1561
1640
|
if (empty) empty.remove();
|
|
1562
1641
|
els.messages.appendChild(messageElement(room, m));
|
|
1563
1642
|
if (m.from === "human") refreshSeen(room);
|
|
1643
|
+
if (m.streaming && m.from !== "human") renderSideRoom();
|
|
1564
1644
|
else if (!stick && m.kind === "chat") noteNew(room, m);
|
|
1565
1645
|
}
|
|
1566
1646
|
if (stick) scrollToBottom();
|
|
@@ -1997,7 +2077,7 @@
|
|
|
1997
2077
|
${field("Emoji", `<div id="rp-emoji-picker"></div><input type="text" id="rp-emoji" maxlength="8" value="${esc(rs.emoji || "")}" placeholder="custom emoji (optional)">`, "A face for the room, next to its name.")}
|
|
1998
2078
|
${field("Topic", `<input type="text" id="rp-topic" maxlength="2000" value="${esc(rs.topic || "")}" placeholder="what this room is about (optional)">`)}
|
|
1999
2079
|
${field("Folder", `<span class="dir-row"><input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false"><button type="button" class="btn ghost browse-btn" id="rp-dir-browse" title="Choose a folder">${ic("folder")}Browse</button></span>`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
2000
|
-
<
|
|
2080
|
+
<div class="field mention-host"><span class="label">Room rules${geekTip("References follow renames and note when a participant has left. Rules go into every vibemate's brief as instructions, not as routing.")}</span><div id="rp-rules" class="rules-editor" contenteditable="true" spellcheck="true" data-placeholder="e.g. Everyone listens to @Pesho, he is the manager. Keep answers under 3 sentences."></div><span class="hint">One rule per line; type @ to reference a participant.</span><div class="mention-menu inline" id="rp-rules-menu" hidden></div></div>
|
|
2001
2081
|
${field("Language", `<input type="text" id="rp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
2002
2082
|
</div>
|
|
2003
2083
|
<div class="section">
|
|
@@ -2007,6 +2087,11 @@
|
|
|
2007
2087
|
<label class="switch"><span class="label">Vibemates wake each other<span class="hint">A reply without @ wakes every other vibemate, as yours does; each may answer or stay silent. Off: only @Name wakes a vibemate. The hop limit applies either way.</span></span><input type="checkbox" id="rp-wake" ${rs.agentsWakeEachOther !== false ? "checked" : ""}></label>
|
|
2008
2088
|
<label class="switch"><span class="label">Wait while you are typing<span class="hint">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></span><input type="checkbox" id="rp-wait-typing" ${rs.waitWhileHumanTypes !== false ? "checked" : ""}></label>
|
|
2009
2089
|
</div>
|
|
2090
|
+
<div class="section template">
|
|
2091
|
+
${sectionTitle("rooms", "Turn this room into a template")}
|
|
2092
|
+
<p class="field-note">Its settings, rules, folder and vibemates (with the coding agent each runs on) become one of your templates, listed first under "Start from a template". You see everything it will contain, and can change any of it, before you create it.</p>
|
|
2093
|
+
<div class="row-btns start stp-row"><button type="button" class="btn sm primary" id="rp-template">${ic("rooms")}Preview and create template</button></div>
|
|
2094
|
+
</div>
|
|
2010
2095
|
${geek(
|
|
2011
2096
|
"rp-geek",
|
|
2012
2097
|
`<div class="section">
|
|
@@ -2034,6 +2119,7 @@
|
|
|
2034
2119
|
<label class="switch"><span class="label">Repeat core rules in every header</span><input type="checkbox" id="rp-header-rules" ${rs.headerRules ? "checked" : ""}></label>
|
|
2035
2120
|
<label class="switch"><span class="label">Show vendor and model to other vibemates</span><input type="checkbox" id="rp-vendor" ${rs.showVendorInRoster ? "checked" : ""}></label>
|
|
2036
2121
|
${field("Replay last N chat messages after a reconnect", `<input type="number" id="rp-replay" min="0" max="200" value="${rs.replayAfterRestart}">`)}
|
|
2122
|
+
${field("Missed messages a vibemate reads at most on its next turn", `<input type="number" id="rp-backlog" min="1" max="1000" value="${rs.backlogCap}">`, "Everything posted since its last turn counts, including while it was muted; older messages are dropped with a note in its prompt.")}
|
|
2037
2123
|
</div>`,
|
|
2038
2124
|
"tools, hops, referee, briefs",
|
|
2039
2125
|
)}
|
|
@@ -2057,6 +2143,7 @@
|
|
|
2057
2143
|
$("#rp-dir").value = dir;
|
|
2058
2144
|
$("#rp-dir").dispatchEvent(new Event("change", { bubbles: true }));
|
|
2059
2145
|
}));
|
|
2146
|
+
$("#rp-template").addEventListener("click", () => openSaveTemplateDialog(room));
|
|
2060
2147
|
bindSave($("#rp-form"), $("#rp-save"), async () => {
|
|
2061
2148
|
const name = $("#rp-name").value;
|
|
2062
2149
|
if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
|
|
@@ -2075,6 +2162,7 @@
|
|
|
2075
2162
|
headerRules: $("#rp-header-rules").checked,
|
|
2076
2163
|
showVendorInRoster: $("#rp-vendor").checked,
|
|
2077
2164
|
replayAfterRestart: Number($("#rp-replay").value),
|
|
2165
|
+
backlogCap: Number($("#rp-backlog").value),
|
|
2078
2166
|
refereeAction: $("#rp-referee").value,
|
|
2079
2167
|
turnTaking: $("#rp-turns").value,
|
|
2080
2168
|
waitWhileHumanTypes: $("#rp-wait-typing").checked,
|
|
@@ -2130,6 +2218,13 @@
|
|
|
2130
2218
|
${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>`)}
|
|
2131
2219
|
${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.")}
|
|
2132
2220
|
</div>
|
|
2221
|
+
<div class="section" id="sp-appearance">
|
|
2222
|
+
${sectionTitle("eye", "Appearance")}
|
|
2223
|
+
${field("Text size, px", `<input type="number" id="sp-chat-fs" min="12" max="24" step="0.5" value="${esc(String((s.appearance || {}).chatFontSize || 14.5))}">`, "The size of the chat text; 14.5 is the default. Everything else in the window scales with it.")}
|
|
2224
|
+
${field("Font", `<select id="sp-font">${Object.entries(FONTS.text).map(([id, f]) => `<option value="${id}"${((s.appearance || {}).font || "nunito") === id ? " selected" : ""}>${esc(f.label)}</option>`).join("")}</select>`, "Nunito, Inter and Noto Sans come with viberoom and look the same on every OS; the system entries use what this machine has.")}
|
|
2225
|
+
${field("Code font", `<select id="sp-mono">${Object.entries(FONTS.mono).map(([id, f]) => `<option value="${id}"${((s.appearance || {}).mono || "jetbrains-mono") === id ? " selected" : ""}>${esc(f.label)}</option>`).join("")}</select>`, "For code blocks, paths and tool output.")}
|
|
2226
|
+
<div class="bubble" id="sp-chat-sample" style="display:inline-block;font-size:${((s.appearance || {}).chatFontSize || 14.5) / zoomFactor()}px">Messages will read like this, with <code>code</code> a step smaller.</div>
|
|
2227
|
+
</div>
|
|
2133
2228
|
<div class="section" id="sp-editor">
|
|
2134
2229
|
${sectionTitle("pencil", "Open files at a line")}
|
|
2135
2230
|
<label class="field"><span class="label">A click on a path like main.ts:375 opens the file in${geekTip("Only an editor can jump to a line; the OS default app just opens the file. Auto looks for VS Code, Cursor, Windsurf, Zed, Sublime Text, Notepad++ and the JetBrains IDEs, in that order, on PATH and in their usual folders. Custom: a command with {file}, {line} and {column} placeholders, e.g. code --goto {file}:{line}.")}</span>
|
|
@@ -2154,6 +2249,12 @@
|
|
|
2154
2249
|
</div>
|
|
2155
2250
|
</div>
|
|
2156
2251
|
<div>
|
|
2252
|
+
<div class="section" id="sp-update">
|
|
2253
|
+
${sectionTitle("refresh", "Updates")}
|
|
2254
|
+
<label class="switch"><span class="label">Check for updates once a day<span class="hint">At start, one request to the npm registry for the latest viberoom version; nothing else leaves this machine. A newer version shows as a bubble over your avatar.</span></span><input type="checkbox" id="sp-updates" ${s.checkForUpdates !== false ? "checked" : ""}></label>
|
|
2255
|
+
<p class="hint" id="sp-update-status">${updateStatusText()}</p>
|
|
2256
|
+
<button type="button" class="btn sm" id="sp-update-check">Check now</button>
|
|
2257
|
+
</div>
|
|
2157
2258
|
<div class="section">
|
|
2158
2259
|
${sectionTitle("spark", "Vibemates on this machine")}
|
|
2159
2260
|
${machine || '<p class="hint">No supported vibemate is installed yet.</p>'}
|
|
@@ -2231,6 +2332,27 @@
|
|
|
2231
2332
|
});
|
|
2232
2333
|
$("#sp-diagram-color").addEventListener("input", redrawPreview);
|
|
2233
2334
|
renderDiagrams(diagramSection, previewTheme());
|
|
2335
|
+
const sample = $("#sp-chat-sample");
|
|
2336
|
+
$("#sp-chat-fs").addEventListener("input", () => {
|
|
2337
|
+
const px = Number($("#sp-chat-fs").value);
|
|
2338
|
+
if (px >= 12 && px <= 24) sample.style.fontSize = `${px / zoomFactor()}px`;
|
|
2339
|
+
});
|
|
2340
|
+
$("#sp-font").addEventListener("change", () => (sample.style.fontFamily = FONTS.text[$("#sp-font").value].stack));
|
|
2341
|
+
$("#sp-mono").addEventListener("change", () => sample.querySelectorAll("code").forEach((c) => (c.style.fontFamily = FONTS.mono[$("#sp-mono").value].stack)));
|
|
2342
|
+
$("#sp-update-check").addEventListener("click", async () => {
|
|
2343
|
+
const b = $("#sp-update-check");
|
|
2344
|
+
b.disabled = true;
|
|
2345
|
+
b.classList.add("loading");
|
|
2346
|
+
try {
|
|
2347
|
+
state.update = await get("/api/update?check=1");
|
|
2348
|
+
$("#sp-update-status").textContent = updateStatusText();
|
|
2349
|
+
renderUpdatePop();
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
showError(e);
|
|
2352
|
+
}
|
|
2353
|
+
b.disabled = false;
|
|
2354
|
+
b.classList.remove("loading");
|
|
2355
|
+
});
|
|
2234
2356
|
bindSave($("#sp-form"), $("#sp-save"), async () => {
|
|
2235
2357
|
const vendorPresets = {};
|
|
2236
2358
|
els.pageInner.querySelectorAll("input[data-vendor]").forEach((inp) => {
|
|
@@ -2240,8 +2362,10 @@
|
|
|
2240
2362
|
await post("/api/settings", {
|
|
2241
2363
|
bypassPermissionsByDefault: $("#sp-bypass").checked,
|
|
2242
2364
|
agentSkillsNeedApproval: $("#sp-skill-approval").checked,
|
|
2365
|
+
checkForUpdates: $("#sp-updates").checked,
|
|
2243
2366
|
diagrams: { preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null },
|
|
2244
2367
|
editor: { mode: $("#sp-editor-mode").value, command: $("#sp-editor-cmd").value },
|
|
2368
|
+
appearance: { chatFontSize: Number($("#sp-chat-fs").value), font: $("#sp-font").value, mono: $("#sp-mono").value },
|
|
2245
2369
|
roomDefaults: {
|
|
2246
2370
|
turnTaking: $("#sp-turns").value,
|
|
2247
2371
|
replyDelay: Number($("#sp-delay").value),
|
|
@@ -2256,6 +2380,16 @@
|
|
|
2256
2380
|
});
|
|
2257
2381
|
}
|
|
2258
2382
|
|
|
2383
|
+
function updateStatusText() {
|
|
2384
|
+
const u = state.update;
|
|
2385
|
+
const v = state.version ? state.version.version : "?";
|
|
2386
|
+
if (!u || !u.checkedAt) return `This is viberoom ${v}; not checked yet.`;
|
|
2387
|
+
const when = new Date(u.checkedAt).toLocaleString();
|
|
2388
|
+
if (u.available) return `viberoom ${u.latest} is available (this is ${u.current}); checked ${when}.`;
|
|
2389
|
+
if (u.error) return `Could not reach the registry (${u.error}); checked ${when}.`;
|
|
2390
|
+
return `This is viberoom ${u.current}, the latest; checked ${when}.`;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2259
2393
|
function skillBadges(sk) {
|
|
2260
2394
|
const out = [];
|
|
2261
2395
|
if (sk.userInvocable === false) out.push('<span class="badge">vibemate only</span>');
|
|
@@ -2802,7 +2936,7 @@
|
|
|
2802
2936
|
const faces = t.vibemates.slice(0, 4).map((v) => avatar({ name: v.name, avatar: v.avatar, color: "#9ca3af" }, 20, {})).join("");
|
|
2803
2937
|
return `<button type="button" class="tpl-item${on ? " on" : ""}" data-id="${esc(t.id)}" role="radio" aria-checked="${on ? "true" : "false"}">
|
|
2804
2938
|
<span class="tpl-emoji">${esc(t.emoji || "🧩")}</span>
|
|
2805
|
-
<span class="tpl-body"><b>${esc(t.name)}${t.recommended ? '<span class="badge tpl-rec">recommended</span>' : ""}</b><span class="tpl-meta">${t.vibemates.length} vibemate${t.vibemates.length === 1 ? "" : "s"}${t.builtin ? " · built in" : "
|
|
2939
|
+
<span class="tpl-body"><b>${esc(t.name)}${t.builtin ? "" : '<span class="badge tpl-mine">your template</span>'}${t.recommended ? '<span class="badge tpl-rec">recommended</span>' : ""}</b><span class="tpl-meta">${t.vibemates.length} vibemate${t.vibemates.length === 1 ? "" : "s"}${t.builtin ? " · built in" : ""}</span><span class="avatar-stack">${faces}</span></span>
|
|
2806
2940
|
<span class="tpl-check">${ic("check")}</span>
|
|
2807
2941
|
</button>`;
|
|
2808
2942
|
})
|
|
@@ -2828,10 +2962,14 @@
|
|
|
2828
2962
|
return `<div class="tpl-vm" data-i="${i}">
|
|
2829
2963
|
<div class="tpl-vm-head"><b>${esc(v.name)}</b>${v.tagline ? `<span class="hint">"${esc(v.tagline)}"</span>` : ""}</div>
|
|
2830
2964
|
${v.role ? `<div class="tpl-vm-role">${esc(v.role)}</div>` : ""}
|
|
2965
|
+
${runsOn(v)}
|
|
2831
2966
|
</div>`;
|
|
2832
2967
|
})
|
|
2833
2968
|
.join("")}`;
|
|
2834
|
-
tplEls.detail.insertAdjacentHTML("beforeend",
|
|
2969
|
+
if (t.dir) tplEls.detail.insertAdjacentHTML("beforeend", `<p class="hint">Folder: <code>${esc(t.dir)}</code> (you can change it below)</p>`);
|
|
2970
|
+
tplEls.detail.insertAdjacentHTML("beforeend", t.vibemates.some((v) => v.agentType)
|
|
2971
|
+
? '<p class="hint">A vibemate whose coding agent is not installed here waits in the roster until you pick another.</p>'
|
|
2972
|
+
: '<p class="hint">You pick the coding agent for each vibemate in the room, right after it opens.</p>');
|
|
2835
2973
|
}
|
|
2836
2974
|
$("#tpl-dir-browse").addEventListener("click", () => openFolderPicker(tplEls.dir.value, (dir) => (tplEls.dir.value = dir)));
|
|
2837
2975
|
tplEls.form.addEventListener("submit", async (event) => {
|
|
@@ -2858,6 +2996,142 @@
|
|
|
2858
2996
|
}
|
|
2859
2997
|
});
|
|
2860
2998
|
|
|
2999
|
+
function runsOn(v) {
|
|
3000
|
+
if (!v.agentType) return "";
|
|
3001
|
+
const rec = state.recipes.find((r) => r.id === v.agentType);
|
|
3002
|
+
const parts = [rec ? rec.vendor : v.agentType, v.model, v.effort, v.mode].filter(Boolean);
|
|
3003
|
+
return `<div class="chips tpl-runs">${parts.map((x) => `<span class="chip">${esc(x)}</span>`).join("")}${rec && rec.unavailableReason ? '<span class="badge status-offline">not installed here</span>' : ""}</div>`;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
const stpEls = { dialog: $("#save-template-dialog"), form: $("#save-template-form"), name: $("#stp-name"), desc: $("#stp-desc"), preview: $("#stp-preview"), error: $("#stp-error"), create: $("#stp-create") };
|
|
3007
|
+
const stp = { room: null, template: null };
|
|
3008
|
+
async function openSaveTemplateDialog(room) {
|
|
3009
|
+
stp.room = room;
|
|
3010
|
+
stpEls.error.hidden = true;
|
|
3011
|
+
stpEls.name.value = room.name;
|
|
3012
|
+
stpEls.desc.value = "";
|
|
3013
|
+
stpEls.preview.innerHTML = '<span class="hint">loading…</span>';
|
|
3014
|
+
openDialog(stpEls.dialog);
|
|
3015
|
+
try {
|
|
3016
|
+
const t = (await post(roomApi("/template-preview"), {})).template;
|
|
3017
|
+
stp.template = t;
|
|
3018
|
+
stpEls.preview.innerHTML = renderTemplateForm(t);
|
|
3019
|
+
stpEls.preview.querySelectorAll("[data-stp-browse]").forEach((b) => b.addEventListener("click", () => {
|
|
3020
|
+
const input = $("#stp-dir");
|
|
3021
|
+
openFolderPicker(input.value, (dir) => (input.value = dir));
|
|
3022
|
+
}));
|
|
3023
|
+
stpEls.preview.querySelectorAll("[data-stp-remove]").forEach((b) => b.addEventListener("click", () => {
|
|
3024
|
+
b.closest(".stp-vm").remove();
|
|
3025
|
+
$("#stp-vm-count").textContent = String(stpEls.preview.querySelectorAll(".stp-vm").length);
|
|
3026
|
+
}));
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
stpEls.error.textContent = error.message;
|
|
3029
|
+
stpEls.error.hidden = false;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const stpField = (label, html, wide) => `<label class="field${wide ? " wide" : ""}"><span class="label">${label}</span>${html}</label>`;
|
|
3033
|
+
const stpSwitch = (label, id, on) => `<label class="switch"><span class="label">${label}</span><input type="checkbox" id="${id}"${on ? " checked" : ""}></label>`;
|
|
3034
|
+
const stpSelect = (id, value, options) => `<select id="${id}">${options.map(([v, l]) => `<option value="${esc(v)}"${String(value) === v ? " selected" : ""}>${esc(l)}</option>`).join("")}</select>`;
|
|
3035
|
+
function renderTemplateForm(t) {
|
|
3036
|
+
const st = t.settings || {};
|
|
3037
|
+
const lang = st.language && st.language.mode === "fixed" ? st.language.language : "";
|
|
3038
|
+
const room = `<div class="stp-grid">
|
|
3039
|
+
${stpField("Emoji", `<input type="text" id="stp-emoji" maxlength="8" value="${esc(t.emoji || "")}" placeholder="none">`)}
|
|
3040
|
+
${stpField("Topic", `<input type="text" id="stp-topic" maxlength="2000" value="${esc(st.topic || "")}" placeholder="what the room is about">`)}
|
|
3041
|
+
${stpField("Language", `<input type="text" id="stp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
3042
|
+
${stpField("Vibemates' own tools", stpSelect("stp-tools", st.tools || "on-request", [["on-request", "Only when someone explicitly asks"], ["never", "Never (chat only)"]]))}
|
|
3043
|
+
${stpField("Who may speak", stpSelect("stp-turns", st.turnTaking || "parallel", [["parallel", "All addressed vibemates at once"], ["one-at-a-time", "One vibemate at a time"]]))}
|
|
3044
|
+
${stpField("Reply delay, seconds", `<input type="number" id="stp-delay" min="0" max="120" step="0.5" value="${esc(String(st.replyDelay ?? 4))}">`)}
|
|
3045
|
+
${stpSwitch("Vibemates wake each other", "stp-wake", st.agentsWakeEachOther !== false)}
|
|
3046
|
+
${stpSwitch("Wait while you are typing", "stp-wait", st.waitWhileHumanTypes !== false)}
|
|
3047
|
+
${stpField("Hop limit", `<input type="number" id="stp-hops" min="0" max="10000" value="${esc(String(st.hopLimit ?? 100))}">`)}
|
|
3048
|
+
${stpField("Max sentences per reply", `<input type="number" id="stp-maxlen" min="1" max="100" value="${st.maxSentences ?? ""}" placeholder="no limit">`)}
|
|
3049
|
+
${stpField("Referee", stpSelect("stp-referee", st.refereeAction || "next-header", [["next-header", "Post it; remind in the next header"], ["retry-hidden", "Hold it; retry in a hidden turn"]]))}
|
|
3050
|
+
${stpField("About you", stpSelect("stp-about", st.humanDescriptionMode || "inherit", [["inherit", "Program-wide description"], ["append", "Program-wide + this room's"], ["override", "Only this room's"], ["none", "Nothing about me"]]))}
|
|
3051
|
+
${stpField("Full brief every N turns", `<input type="number" id="stp-brief-turns" min="1" max="10000" value="${esc(String(st.fullBriefEveryTurns ?? 8))}">`)}
|
|
3052
|
+
${stpField("…or every N tokens", `<input type="number" id="stp-brief-tokens" min="1000" max="10000000" step="1000" value="${esc(String(st.fullBriefEveryTokens ?? 20000))}">`)}
|
|
3053
|
+
${stpField("Replay after a reconnect", `<input type="number" id="stp-replay" min="0" max="200" value="${esc(String(st.replayAfterRestart ?? 10))}">`)}
|
|
3054
|
+
${stpField("Missed messages read at most", `<input type="number" id="stp-backlog" min="1" max="1000" value="${esc(String(st.backlogCap ?? 50))}">`)}
|
|
3055
|
+
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
3056
|
+
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
3057
|
+
</div>`;
|
|
3058
|
+
const agentOptions = [["", "none yet: cast when the room opens"], ...state.recipes.map((r) => [r.id, r.vendor + (r.unavailableReason ? " (not installed here)" : "")])];
|
|
3059
|
+
const vms = (t.vibemates || []).map((v, i) => `<div class="stp-vm" data-i="${i}">${avatar({ name: v.name, avatar: v.avatar, color: "#9ca3af" }, 36, {})}<div>
|
|
3060
|
+
<div class="stp-vm-top"><b>${esc(v.name)}</b><button type="button" class="icon-btn sm ghost" title="Leave this vibemate out of the template" data-stp-remove>${ic("close")}</button></div>
|
|
3061
|
+
<div class="stp-vm-fields">
|
|
3062
|
+
${stpField("Vibename", `<input type="text" data-k="name" maxlength="40" value="${esc(v.name)}" required>`)}
|
|
3063
|
+
${stpField("Vibersona", `<input type="text" data-k="tagline" maxlength="80" value="${esc(v.tagline || "")}">`)}
|
|
3064
|
+
${stpField("Vibio", `<textarea data-k="role" rows="3" maxlength="4000">${esc(v.role || "")}</textarea>`, true)}
|
|
3065
|
+
${stpField("Vibeface", `<input type="text" data-k="avatar" maxlength="8" value="${esc(v.avatar || "")}" placeholder="initials">`)}
|
|
3066
|
+
${stpField("Coding agent", stpSelect("", v.agentType || "", agentOptions).replace('id=""', 'data-k="agentType"'))}
|
|
3067
|
+
${stpField("Model", `<input type="text" data-k="model" value="${esc(v.model || "")}" placeholder="the agent's default">`)}
|
|
3068
|
+
${stpField("Effort", `<input type="text" data-k="effort" value="${esc(v.effort || "")}" placeholder="default">`)}
|
|
3069
|
+
${stpField("Mode", `<input type="text" data-k="mode" value="${esc(v.mode || "")}" placeholder="default">`)}
|
|
3070
|
+
${stpField("Reply delay override, s", `<input type="number" data-k="replyDelay" min="0" max="120" step="0.5" value="${v.replyDelay ?? ""}" placeholder="the room's">`)}
|
|
3071
|
+
${stpField("Skills, comma-separated", `<input type="text" data-k="skills" value="${esc((v.skills || []).join(", "))}">`, true)}
|
|
3072
|
+
</div>
|
|
3073
|
+
</div></div>`).join("");
|
|
3074
|
+
return `
|
|
3075
|
+
<div class="stp-section"><h5>Room</h5>${room}</div>
|
|
3076
|
+
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5" maxlength="4000" placeholder="one rule per line">${esc(st.customRules || "")}</textarea></div>
|
|
3077
|
+
<div class="stp-section"><h5>Folder</h5><span class="dir-row"><input type="text" id="stp-dir" maxlength="1000" value="${esc(t.dir || "")}" spellcheck="false"><button type="button" class="btn ghost browse-btn" data-stp-browse>${ic("folder")}Browse</button></span></div>
|
|
3078
|
+
<div class="stp-section"><h5>Vibemates · <span id="stp-vm-count">${(t.vibemates || []).length}</span></h5>${vms || '<span class="hint">none</span>'}</div>`;
|
|
3079
|
+
}
|
|
3080
|
+
function readTemplateForm() {
|
|
3081
|
+
const num = (id) => Number($(id).value);
|
|
3082
|
+
const langText = $("#stp-lang").value.trim();
|
|
3083
|
+
const settings = {
|
|
3084
|
+
topic: $("#stp-topic").value,
|
|
3085
|
+
language: langText ? { mode: "fixed", language: langText } : { mode: "follow-human" },
|
|
3086
|
+
tools: $("#stp-tools").value,
|
|
3087
|
+
turnTaking: $("#stp-turns").value,
|
|
3088
|
+
replyDelay: num("#stp-delay"),
|
|
3089
|
+
agentsWakeEachOther: $("#stp-wake").checked,
|
|
3090
|
+
waitWhileHumanTypes: $("#stp-wait").checked,
|
|
3091
|
+
hopLimit: num("#stp-hops"),
|
|
3092
|
+
maxSentences: $("#stp-maxlen").value === "" ? null : num("#stp-maxlen"),
|
|
3093
|
+
refereeAction: $("#stp-referee").value,
|
|
3094
|
+
humanDescriptionMode: $("#stp-about").value,
|
|
3095
|
+
fullBriefEveryTurns: num("#stp-brief-turns"),
|
|
3096
|
+
fullBriefEveryTokens: num("#stp-brief-tokens"),
|
|
3097
|
+
replayAfterRestart: num("#stp-replay"),
|
|
3098
|
+
backlogCap: num("#stp-backlog"),
|
|
3099
|
+
headerRules: $("#stp-header-rules").checked,
|
|
3100
|
+
showVendorInRoster: $("#stp-vendor").checked,
|
|
3101
|
+
customRules: $("#stp-rules").value.slice(0, 4000),
|
|
3102
|
+
};
|
|
3103
|
+
const vibemates = [...stpEls.preview.querySelectorAll(".stp-vm")].map((row) => {
|
|
3104
|
+
const v = {};
|
|
3105
|
+
row.querySelectorAll("[data-k]").forEach((el) => {
|
|
3106
|
+
const k = el.dataset.k;
|
|
3107
|
+
const val = el.value.trim();
|
|
3108
|
+
if (k === "skills") v.skills = val.split(",").map((x) => x.trim()).filter(Boolean);
|
|
3109
|
+
else if (k === "replyDelay") { if (val !== "") v.replyDelay = Number(val); }
|
|
3110
|
+
else if (val) v[k] = val;
|
|
3111
|
+
});
|
|
3112
|
+
return v;
|
|
3113
|
+
});
|
|
3114
|
+
return { emoji: $("#stp-emoji").value.trim(), dir: $("#stp-dir").value.trim(), settings, vibemates };
|
|
3115
|
+
}
|
|
3116
|
+
stpEls.form.addEventListener("submit", async (event) => {
|
|
3117
|
+
event.preventDefault();
|
|
3118
|
+
if (!stp.room) return;
|
|
3119
|
+
stpEls.create.disabled = true;
|
|
3120
|
+
stpEls.create.textContent = "Creating…";
|
|
3121
|
+
try {
|
|
3122
|
+
const edited = readTemplateForm();
|
|
3123
|
+
const res = await post(`/api/rooms/${encodeURIComponent(stp.room.id)}/save-template`, { name: stpEls.name.value, description: stpEls.desc.value, emoji: edited.emoji, template: { dir: edited.dir, settings: edited.settings, vibemates: edited.vibemates } });
|
|
3124
|
+
closeDialog(stpEls.dialog);
|
|
3125
|
+
toast(`Template "${res.template.name}" created. It is first under "Start from a template".`, "success");
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
stpEls.error.textContent = error.message;
|
|
3128
|
+
stpEls.error.hidden = false;
|
|
3129
|
+
} finally {
|
|
3130
|
+
stpEls.create.disabled = false;
|
|
3131
|
+
stpEls.create.textContent = "Create the template";
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
3134
|
+
|
|
2861
3135
|
function openRoomDialog() {
|
|
2862
3136
|
els.roomError.hidden = true;
|
|
2863
3137
|
els.roomName.value = "";
|
|
@@ -2953,8 +3227,20 @@
|
|
|
2953
3227
|
}
|
|
2954
3228
|
|
|
2955
3229
|
|
|
3230
|
+
function applyAppearance() {
|
|
3231
|
+
const a = (state.settings || {}).appearance || {};
|
|
3232
|
+
const root = document.documentElement;
|
|
3233
|
+
root.style.zoom = String((a.chatFontSize || 14.5) / 14.5);
|
|
3234
|
+
root.style.setProperty("--font", (FONTS.text[a.font] || FONTS.text.nunito).stack);
|
|
3235
|
+
root.style.setProperty("--mono", (FONTS.mono[a.mono] || FONTS.mono["jetbrains-mono"]).stack);
|
|
3236
|
+
}
|
|
3237
|
+
const zoomFactor = () => Number(document.documentElement.style.zoom) || 1;
|
|
3238
|
+
|
|
2956
3239
|
function loadSnapshot(snapshot) {
|
|
2957
3240
|
state.settings = snapshot.settings;
|
|
3241
|
+
applyAppearance();
|
|
3242
|
+
state.update = snapshot.update || null;
|
|
3243
|
+
renderUpdatePop();
|
|
2958
3244
|
state.version = snapshot.version || null;
|
|
2959
3245
|
state.skills = snapshot.skills || [];
|
|
2960
3246
|
state.recipes = snapshot.recipes || [];
|
|
@@ -3131,12 +3417,18 @@
|
|
|
3131
3417
|
});
|
|
3132
3418
|
es.addEventListener("settings", (e) => {
|
|
3133
3419
|
state.settings = JSON.parse(e.data).settings;
|
|
3420
|
+
applyAppearance();
|
|
3134
3421
|
rerenderDiagrams();
|
|
3135
3422
|
renderRail();
|
|
3136
3423
|
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
3137
3424
|
if (state.detailsOpen && state.selection.kind === "me" && !editingInDetails()) renderDetails();
|
|
3138
3425
|
if (state.view === "room") renderSideRoom();
|
|
3139
3426
|
});
|
|
3427
|
+
es.addEventListener("update", (e) => {
|
|
3428
|
+
state.update = JSON.parse(e.data).update;
|
|
3429
|
+
renderUpdatePop();
|
|
3430
|
+
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
3431
|
+
});
|
|
3140
3432
|
es.addEventListener("reset", () => location.href = "/");
|
|
3141
3433
|
}
|
|
3142
3434
|
function releaseStream() {
|
|
@@ -3178,14 +3470,14 @@
|
|
|
3178
3470
|
let drag = null;
|
|
3179
3471
|
grip.addEventListener("pointerdown", (e) => {
|
|
3180
3472
|
if (e.button !== 0) return;
|
|
3181
|
-
drag = { y: e.clientY, h: els.input.offsetHeight };
|
|
3473
|
+
drag = { y: e.clientY / zoomFactor(), h: els.input.offsetHeight };
|
|
3182
3474
|
grip.setPointerCapture(e.pointerId);
|
|
3183
3475
|
els.composer.classList.add("resizing");
|
|
3184
3476
|
e.preventDefault();
|
|
3185
3477
|
});
|
|
3186
3478
|
grip.addEventListener("pointermove", (e) => {
|
|
3187
3479
|
if (!drag) return;
|
|
3188
|
-
composerMin = Math.round(Math.min(composerCeiling(), Math.max(36, drag.h + drag.y - e.clientY)));
|
|
3480
|
+
composerMin = Math.round(Math.min(composerCeiling(), Math.max(36, drag.h + drag.y - e.clientY / zoomFactor())));
|
|
3189
3481
|
autosize();
|
|
3190
3482
|
});
|
|
3191
3483
|
const stop = () => {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|