viberoom 0.7.0 → 0.9.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/NOTICE +5 -3
- package/README.md +90 -19
- package/assets/vendors/grok.svg +3 -0
- package/assets/vendors/hermes.svg +1 -0
- package/dist/agent-health.js +1 -1
- package/dist/hub.js +98 -49
- package/dist/jsonrpc.js +3 -1
- package/dist/look-lint.js +180 -0
- package/dist/looks.js +151 -0
- package/dist/main.js +4 -1
- package/dist/mcp-skills-server.js +87 -0
- package/dist/persona.js +10 -3
- package/dist/recipes.js +110 -8
- package/dist/room-design.js +9 -6
- package/dist/room.js +190 -26
- package/dist/server.js +73 -1
- package/dist/skills.js +23 -2
- package/package.json +1 -1
- package/ui/app.css +23 -16
- package/ui/app.js +198 -66
- package/ui/components.css +96 -115
- package/ui/components.js +8 -3
- package/ui/index.html +3 -2
- package/ui/looks.css +462 -77
- package/ui/theme.css +94 -13
- package/ui/tokens.js +526 -57
package/ui/app.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
recipes: [],
|
|
8
8
|
roomDefaults: null,
|
|
9
9
|
skills: [],
|
|
10
|
+
looks: [],
|
|
10
11
|
version: null,
|
|
11
12
|
rooms: new Map(),
|
|
12
13
|
currentRoomId: null,
|
|
@@ -161,36 +162,7 @@
|
|
|
161
162
|
} catch {
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const FONTS =
|
|
165
|
-
text: {
|
|
166
|
-
nunito: { label: "Nunito (default)", stack: '"Nunito", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
167
|
-
inter: { label: "Inter", stack: '"Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
168
|
-
"noto-sans": { label: "Noto Sans", stack: '"Noto Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
169
|
-
"open-sans": { label: "Open Sans", stack: '"Open Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
170
|
-
"source-sans-3": { label: "Source Sans 3", stack: '"Source Sans 3", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
171
|
-
"ibm-plex-sans": { label: "IBM Plex Sans", stack: '"IBM Plex Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
172
|
-
"manrope": { label: "Manrope", stack: '"Manrope", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
173
|
-
"rubik": { label: "Rubik", stack: '"Rubik", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
174
|
-
"montserrat": { label: "Montserrat", stack: '"Montserrat", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
175
|
-
"golos-text": { label: "Golos Text", stack: '"Golos Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
176
|
-
"exo-2": { label: "Exo 2", stack: '"Exo 2", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
177
|
-
"comfortaa": { label: "Comfortaa", stack: '"Comfortaa", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
178
|
-
"ubuntu-sans": { label: "Ubuntu Sans", stack: '"Ubuntu Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
179
|
-
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
180
|
-
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
181
|
-
},
|
|
182
|
-
mono: {
|
|
183
|
-
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
184
|
-
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
185
|
-
"source-code-pro": { label: "Source Code Pro", stack: '"Source Code Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
186
|
-
"ibm-plex-mono": { label: "IBM Plex Mono", stack: '"IBM Plex Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
187
|
-
"pt-mono": { label: "PT Mono", stack: '"PT Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
188
|
-
"victor-mono": { label: "Victor Mono", stack: '"Victor Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
189
|
-
"anonymous-pro": { label: "Anonymous Pro", stack: '"Anonymous Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
190
|
-
"cascadia-code": { label: "Cascadia Code", stack: '"Cascadia Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
191
|
-
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
192
|
-
},
|
|
193
|
-
};
|
|
165
|
+
const FONTS = globalThis.VIBEROOM_TOKENS.fonts;
|
|
194
166
|
const STATUS_LABEL = { unstaffed: "needs a coding agent", starting: "starting…", idle: "ready", queued: "waiting…", thinking: "thinking…", writing: "writing…", error: "error", offline: "offline", left: "left" };
|
|
195
167
|
const STATUS_TONE = { idle: "ready", queued: "waiting", starting: "waiting", thinking: "thinking", writing: "writing", error: "error", offline: "asleep", left: "asleep", unstaffed: "attention" };
|
|
196
168
|
const TOOL_STATUSES = new Set(["pending", "in_progress", "completed", "failed"]);
|
|
@@ -713,28 +685,34 @@
|
|
|
713
685
|
}
|
|
714
686
|
if (!found.size) return;
|
|
715
687
|
const paths = await Promise.all([...found.keys()].map((token) => resolveInRoom(room.id, token)));
|
|
716
|
-
|
|
688
|
+
const resolved = new Map();
|
|
717
689
|
[...found.keys()].forEach((token, i) => {
|
|
718
690
|
const full = paths[i] && paths[i].path;
|
|
719
|
-
if (
|
|
720
|
-
const byNode = new Map();
|
|
721
|
-
for (const hit of found.get(token)) byNode.set(hit.node, [...(byNode.get(hit.node) || []), hit]);
|
|
722
|
-
for (const [node, hits] of byNode) {
|
|
723
|
-
if (!node.isConnected) continue;
|
|
724
|
-
for (const hit of hits.sort((a, b) => b.index - a.index)) {
|
|
725
|
-
const after = node.splitText(hit.index);
|
|
726
|
-
after.nodeValue = after.nodeValue.slice(hit.length);
|
|
727
|
-
const link = document.createElement("a");
|
|
728
|
-
link.className = "open-link";
|
|
729
|
-
link.href = "#";
|
|
730
|
-
link.dataset.open = `${full}${hit.line}`;
|
|
731
|
-
link.title = `${full} (relative to the room's folder)`;
|
|
732
|
-
link.textContent = `${token}${hit.line}`;
|
|
733
|
-
node.parentNode.insertBefore(link, after);
|
|
734
|
-
linked = true;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
691
|
+
if (full) resolved.set(token, full);
|
|
737
692
|
});
|
|
693
|
+
if (!resolved.size) return;
|
|
694
|
+
const byNode = new Map();
|
|
695
|
+
for (const [token, hits] of found) {
|
|
696
|
+
if (!resolved.has(token)) continue;
|
|
697
|
+
for (const hit of hits) byNode.set(hit.node, [...(byNode.get(hit.node) || []), { ...hit, token }]);
|
|
698
|
+
}
|
|
699
|
+
let linked = false;
|
|
700
|
+
for (const [node, hits] of byNode) {
|
|
701
|
+
if (!node.isConnected) continue;
|
|
702
|
+
for (const hit of hits.sort((a, b) => b.index - a.index)) {
|
|
703
|
+
const full = resolved.get(hit.token);
|
|
704
|
+
const after = node.splitText(hit.index);
|
|
705
|
+
after.nodeValue = after.nodeValue.slice(hit.length);
|
|
706
|
+
const link = document.createElement("a");
|
|
707
|
+
link.className = "open-link";
|
|
708
|
+
link.href = "#";
|
|
709
|
+
link.dataset.open = `${full}${hit.line}`;
|
|
710
|
+
link.title = `${full} (relative to the room's folder)`;
|
|
711
|
+
link.textContent = `${hit.token}${hit.line}`;
|
|
712
|
+
node.parentNode.insertBefore(link, after);
|
|
713
|
+
linked = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
738
716
|
if (linked) renderPreviews(textEl, m);
|
|
739
717
|
}
|
|
740
718
|
function csvTable(rows) {
|
|
@@ -1134,8 +1112,8 @@
|
|
|
1134
1112
|
const field = lastField;
|
|
1135
1113
|
lastField = null;
|
|
1136
1114
|
try {
|
|
1137
|
-
await onSave();
|
|
1138
|
-
if (field) {
|
|
1115
|
+
const outcome = await onSave();
|
|
1116
|
+
if (field && outcome !== false) {
|
|
1139
1117
|
recentlySaved.set(field, Date.now());
|
|
1140
1118
|
markSaved(field);
|
|
1141
1119
|
}
|
|
@@ -1337,24 +1315,60 @@
|
|
|
1337
1315
|
}, 190);
|
|
1338
1316
|
}
|
|
1339
1317
|
function confirmDialog(text, opts) {
|
|
1318
|
+
return choiceDialog(text, opts).then((choice) => choice === "ok");
|
|
1319
|
+
}
|
|
1320
|
+
function choiceDialog(text, opts) {
|
|
1340
1321
|
const o = opts || {};
|
|
1341
1322
|
const dialog = $("#confirm-dialog");
|
|
1342
1323
|
$("#cf-title").textContent = o.title || "Are you sure?";
|
|
1343
1324
|
$("#cf-text").textContent = text;
|
|
1344
|
-
const
|
|
1345
|
-
ok.textContent = o.okLabel || "OK";
|
|
1346
|
-
|
|
1325
|
+
const buttons = { ok: $("#cf-ok"), alt: $("#cf-alt"), cancel: $("#cf-cancel") };
|
|
1326
|
+
buttons.ok.textContent = o.okLabel || "OK";
|
|
1327
|
+
buttons.cancel.textContent = o.cancelLabel || "Cancel";
|
|
1328
|
+
buttons.alt.textContent = o.altLabel || "";
|
|
1329
|
+
buttons.alt.hidden = !o.altLabel;
|
|
1330
|
+
dialog.classList.toggle("three-way", !!o.altLabel);
|
|
1331
|
+
const primary = o.primary || "ok";
|
|
1332
|
+
for (const [name, button] of Object.entries(buttons)) button.dataset.kind = name === primary ? (o.danger ? "danger" : "primary") : "ghost";
|
|
1347
1333
|
return new Promise((resolve) => {
|
|
1348
1334
|
const done = () => {
|
|
1349
1335
|
dialog.removeEventListener("close", done);
|
|
1350
|
-
resolve(dialog.returnValue === "ok");
|
|
1336
|
+
resolve(dialog.returnValue === "ok" ? "ok" : dialog.returnValue === "alt" ? "alt" : "cancel");
|
|
1351
1337
|
};
|
|
1352
1338
|
dialog.addEventListener("close", done);
|
|
1353
1339
|
dialog.returnValue = "";
|
|
1354
1340
|
openDialog(dialog);
|
|
1355
|
-
|
|
1341
|
+
buttons[primary].focus();
|
|
1356
1342
|
});
|
|
1357
1343
|
}
|
|
1344
|
+
|
|
1345
|
+
const BRIEF_TEXT_MAX = 32000;
|
|
1346
|
+
function briefTextLimit(room) {
|
|
1347
|
+
const r = room || currentRoom();
|
|
1348
|
+
return (r && r.settings && Number(r.settings.briefTextLimit)) || 8000;
|
|
1349
|
+
}
|
|
1350
|
+
function bindCount(el, countEl, limitOf) {
|
|
1351
|
+
if (!el || !countEl) return;
|
|
1352
|
+
const tick = () => {
|
|
1353
|
+
const n = (el.isContentEditable ? rulesText(el) : el.value).length;
|
|
1354
|
+
const limit = limitOf();
|
|
1355
|
+
countEl.textContent = `${n} / ${limit}`;
|
|
1356
|
+
countEl.classList.toggle("over", n > limit);
|
|
1357
|
+
};
|
|
1358
|
+
el.addEventListener("input", tick);
|
|
1359
|
+
tick();
|
|
1360
|
+
}
|
|
1361
|
+
async function fitBriefText(text, what, limit, limitInput) {
|
|
1362
|
+
if (text.length <= limit) return text;
|
|
1363
|
+
const needed = Math.min(BRIEF_TEXT_MAX, Math.ceil(text.length / 500) * 500);
|
|
1364
|
+
const canRaise = text.length <= BRIEF_TEXT_MAX;
|
|
1365
|
+
const choice = await choiceDialog(`${what} is ${text.length} characters; this room's limit is ${limit}. Cut it at the limit, or raise the limit for this room?${canRaise ? "" : ` ${BRIEF_TEXT_MAX} is the most a room can allow.`}`, { title: "Over the room's limit", okLabel: `Cut at ${limit}`, altLabel: canRaise ? `Raise the limit to ${needed}` : "", cancelLabel: "Go back", primary: "cancel" });
|
|
1366
|
+
if (choice === "ok") return text.slice(0, limit);
|
|
1367
|
+
if (choice !== "alt") return null;
|
|
1368
|
+
if (limitInput) limitInput.value = String(needed);
|
|
1369
|
+
else await post(roomApi("/settings"), { briefTextLimit: needed });
|
|
1370
|
+
return text;
|
|
1371
|
+
}
|
|
1358
1372
|
function remember(key, value) {
|
|
1359
1373
|
try {
|
|
1360
1374
|
localStorage.setItem(`viberoom.${key}`, String(value));
|
|
@@ -2149,6 +2163,7 @@
|
|
|
2149
2163
|
const card = choice.closest('[data-ui="ask-card"]');
|
|
2150
2164
|
if (choice.dataset.act === "permit") post(roomApi(`/permissions/${encodeURIComponent(card.dataset.key)}`), { optionId: choice.dataset.option || null }).catch(showError);
|
|
2151
2165
|
else if (choice.dataset.act === "decide") post(roomApi(`/proposals/${encodeURIComponent(card.dataset.key)}`), { accept: choice.dataset.answer === "apply" }).catch(showError);
|
|
2166
|
+
else if (choice.dataset.act === "try-look") tryLook(choice.dataset.look);
|
|
2152
2167
|
return;
|
|
2153
2168
|
}
|
|
2154
2169
|
const chip = e.target.closest('[data-ui="tool-call"] > [data-ui="chip"]');
|
|
@@ -2630,6 +2645,7 @@
|
|
|
2630
2645
|
rows.push(`<div class="prop-row"><b>Room rules</b>${lines.join("")}</div>`);
|
|
2631
2646
|
} else rows.push(`<div class="prop-row"><b>${esc(c.key)}</b> <span class="prop-from">${esc(proposalValue(c.from))}</span> → <span class="prop-to">${esc(proposalValue(c.to))}</span></div>`);
|
|
2632
2647
|
}
|
|
2648
|
+
for (const c of p.appearance || []) rows.push(`<div class="prop-row"><b>${esc(c.key)}</b> <span class="prop-from">${esc(proposalValue(c.from))}</span> → <span class="prop-to">${esc(proposalValue(c.to))}</span></div>`);
|
|
2633
2649
|
for (const v of p.vibemates || []) {
|
|
2634
2650
|
if (v.op === "update") rows.push(`<div class="prop-row"><b>${esc(v.name)}</b>${(v.fields || []).map((f) => `<div class="prop-line">${esc(f.field)}: <span class="prop-from">${esc(f.from || "(empty)")}</span> → <span class="prop-to">${esc(f.to || "(empty)")}</span></div>`).join("")}</div>`);
|
|
2635
2651
|
else rows.push(`<div class="prop-row"><b>${v.op === "add" ? "New vibemate" : "Remove"}</b> ${esc(v.name)}${v.op === "add" ? ' <span class="hint">(you pick its coding agent)</span>' : ""}</div>`);
|
|
@@ -2638,9 +2654,15 @@
|
|
|
2638
2654
|
}
|
|
2639
2655
|
function renderProposal(room, p) {
|
|
2640
2656
|
const existing = els.messages.querySelector(`[data-ui="ask-card"][data-kind="proposal"][data-key="${CSS.escape(p.key)}"]`);
|
|
2657
|
+
const lookRow = (p.appearance || []).find((c) => c.key === "look");
|
|
2658
|
+
const lookTarget = lookRow && TOKENS.looks[lookRow.to];
|
|
2659
|
+
const windowNote = p.appearance && p.appearance.length
|
|
2660
|
+
? `<div class="prop-window"><span class="note">${ic("eye")} This changes the whole window, not this room alone.</span>${lookTarget ? `<div class="prop-look">${UI.html("look-card", { look: lookTarget, tag: lookTag(lookTarget), title: lookTarget.label })}${p.status === "pending" ? UI.html("button", { label: document.documentElement.dataset.tried === lookTarget.id ? "Back" : "Try it on", kind: "soft", size: "xs", act: "try-look", data: { look: lookTarget.id }, title: "This window only, until you apply or reject" }) : ""}</div>` : ""}</div>`
|
|
2661
|
+
: "";
|
|
2641
2662
|
const body =
|
|
2642
2663
|
(p.why ? `<div class="prop-why">${esc(p.why)}</div>` : "") +
|
|
2643
2664
|
`<div class="prop-diff">${proposalDiffHtml(p)}</div>` +
|
|
2665
|
+
windowNote +
|
|
2644
2666
|
(p.warnings && p.warnings.length ? `<ul class="prop-warn">${p.warnings.map((w) => `<li>${esc(w)}</li>`).join("")}</ul>` : "") +
|
|
2645
2667
|
(p.touchesOwn ? `<div class="prop-own">${ic("info")} This changes the rules or ${esc(p.participantName)}'s own persona: it decides how ${esc(p.participantName)} itself will behave.</div>` : "");
|
|
2646
2668
|
const outcome = p.status === "pending" ? undefined : p.status === "applied" ? `applied${p.skipped && p.skipped.length ? ` · not applied: ${p.skipped.join("; ")}` : ""}` : "rejected";
|
|
@@ -2753,7 +2775,7 @@
|
|
|
2753
2775
|
${field("Vibename", `<input type="text" id="pp-name" maxlength="24" value="${esc(p.name)}">`)}
|
|
2754
2776
|
${field("Vibersona", `<input type="text" id="pp-tagline" maxlength="80" value="${esc(p.tagline || "")}" placeholder="a few words under the vibename">`, "Shown under the vibename.", "Everyone in the room sees it: you, and the other vibemates in their roster.")}
|
|
2755
2777
|
${field("Vibeface", `<div id="pp-avatar-picker"></div><input type="text" id="pp-avatar" maxlength="8" value="${esc(p.avatar || "")}" placeholder="custom emoji (optional)">`)}
|
|
2756
|
-
${field("Vibio", `<textarea id="pp-role" rows="5"
|
|
2778
|
+
${field("Vibio", `<textarea id="pp-role" rows="5" placeholder="who it is, how it speaks, what it cares about">${esc(p.role || "")}</textarea>`, `Only this vibemate reads it.<span class="count" id="pp-role-count"></span>`, "Reaches the vibemate as refreshed instructions in its brief on its next turn; its memory is kept. The other participants never see it. A vibio and the room rules go into every brief, so the room has a limit for them (the room's settings, for geeks); text over it is never cut in silence.")}
|
|
2757
2779
|
</div>
|
|
2758
2780
|
${p.trouble ? `<div class="trouble"><b>${esc(p.trouble.what)}</b><span>${esc(p.trouble.advice)}</span></div>` : ""}
|
|
2759
2781
|
${p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<p class="hint" style="color:var(--danger);margin:0 4px 10px">${esc(p.statusDetail)}</p>` : ""}
|
|
@@ -2826,7 +2848,13 @@
|
|
|
2826
2848
|
renderSkillChecks($("#pp-skills"), p.skills || []);
|
|
2827
2849
|
bindSave($("#pp-skills-section"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { skills: checkedSkills($("#pp-skills")) }));
|
|
2828
2850
|
bindSave($("#pp-timing"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { replyDelay: $("#pp-delay").value === "" ? null : Number($("#pp-delay").value) }));
|
|
2829
|
-
|
|
2851
|
+
bindCount($("#pp-role"), $("#pp-role-count"), () => briefTextLimit(currentRoom()));
|
|
2852
|
+
bindSave($("#pp-persona"), async () => {
|
|
2853
|
+
const role = await fitBriefText($("#pp-role").value, `${p.name}'s vibio`, briefTextLimit(currentRoom()));
|
|
2854
|
+
if (role === null) return false;
|
|
2855
|
+
await post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { name: $("#pp-name").value, tagline: $("#pp-tagline").value, role, avatar: $("#pp-avatar").value });
|
|
2856
|
+
return true;
|
|
2857
|
+
});
|
|
2830
2858
|
renderConfig($("#pp-config"), p, offline);
|
|
2831
2859
|
}
|
|
2832
2860
|
|
|
@@ -2927,7 +2955,7 @@
|
|
|
2927
2955
|
${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.")}
|
|
2928
2956
|
${field("Topic", `<input type="text" id="rp-topic" maxlength="2000" value="${esc(rs.topic || "")}" placeholder="what this room is about (optional)">`)}
|
|
2929
2957
|
${field("Folder", `<span class="dir-row"><input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false">${UI.html("button", { label: "Browse", icon: "folder", kind: "ghost", id: "rp-dir-browse", title: "Choose a folder", hook: "browse-btn" })}</span>`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
2930
|
-
<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
|
|
2958
|
+
<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 class="count" id="rp-rules-count"></span></span><div class="mention-menu inline" id="rp-rules-menu" hidden></div></div>
|
|
2931
2959
|
${field("Language", `<input type="text" id="rp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
2932
2960
|
</div>
|
|
2933
2961
|
<div class="section">
|
|
@@ -2970,6 +2998,7 @@
|
|
|
2970
2998
|
<label class="switch"><span class="label">Show vendor and model to other vibemates</span><input type="checkbox" id="rp-vendor" ${rs.showVendorInRoster ? "checked" : ""}></label>
|
|
2971
2999
|
${field("Replay last N chat messages after a reconnect", `${UI.html("number-field", { id: "rp-replay", value: String(rs.replayAfterRestart), min: 0, max: 200 })}`)}
|
|
2972
3000
|
${field("Missed messages a vibemate reads at most on its next turn", `${UI.html("number-field", { id: "rp-backlog", value: String(rs.backlogCap), min: 1, max: 1000 })}`, "Everything posted since its last turn counts, including while it was muted; older messages are dropped with a note in its prompt.")}
|
|
3001
|
+
${field("Most characters in a vibio or in the room rules", `${UI.html("number-field", { id: "rp-text-limit", value: String(rs.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`, "Both go into every brief. Text over the limit is refused with the numbers, never cut.")}
|
|
2973
3002
|
</div>`,
|
|
2974
3003
|
"tools, hops, referee, briefs",
|
|
2975
3004
|
)}
|
|
@@ -2983,6 +3012,8 @@
|
|
|
2983
3012
|
wireDetailsClose();
|
|
2984
3013
|
rulesToNodes($("#rp-rules"), room.customRulesText != null ? room.customRulesText : rs.customRules || "", room);
|
|
2985
3014
|
attachRichMentions($("#rp-rules"), $("#rp-rules-menu"));
|
|
3015
|
+
bindCount($("#rp-rules"), $("#rp-rules-count"), () => Number($("#rp-text-limit").value) || briefTextLimit(room));
|
|
3016
|
+
$("#rp-text-limit").addEventListener("input", () => $("#rp-rules").dispatchEvent(new Event("input")));
|
|
2986
3017
|
$("#rp-emoji-picker").appendChild(
|
|
2987
3018
|
emojiGrid(ROOM_EMOJI, rs.emoji || "", (emoji) => {
|
|
2988
3019
|
$("#rp-emoji").value = emoji;
|
|
@@ -2999,10 +3030,13 @@
|
|
|
2999
3030
|
if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
|
|
3000
3031
|
const dir = $("#rp-dir").value.trim();
|
|
3001
3032
|
if (dir && dir !== room.dir) await post(roomApi("/dir"), { dir });
|
|
3033
|
+
const rules = await fitBriefText(rulesText($("#rp-rules")), "The room rules text", Number($("#rp-text-limit").value) || briefTextLimit(room), $("#rp-text-limit"));
|
|
3034
|
+
if (rules === null) return false;
|
|
3002
3035
|
await post(roomApi("/settings"), {
|
|
3003
3036
|
emoji: $("#rp-emoji").value,
|
|
3004
3037
|
topic: $("#rp-topic").value,
|
|
3005
|
-
customRules:
|
|
3038
|
+
customRules: rules,
|
|
3039
|
+
briefTextLimit: Number($("#rp-text-limit").value),
|
|
3006
3040
|
language: $("#rp-lang").value.trim() || "follow-human",
|
|
3007
3041
|
tools: $("#rp-tools").value,
|
|
3008
3042
|
maxSentences: $("#rp-maxlen").value === "" ? null : Number($("#rp-maxlen").value),
|
|
@@ -3072,11 +3106,11 @@
|
|
|
3072
3106
|
const lookId = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
3073
3107
|
const look = TOKENS.looks[lookId];
|
|
3074
3108
|
const custom = (a.custom || {})[lookId] || {};
|
|
3075
|
-
const cards = Object.values(TOKENS.looks).map((l) => UI.html("look-card", { look: l, on: lookId === l.id, data: { look: l.id } })).join("");
|
|
3109
|
+
const cards = Object.values(TOKENS.looks).map((l) => UI.html("look-card", { look: l, tag: lookTag(l), on: lookId === l.id, data: { look: l.id } })).join("");
|
|
3076
3110
|
const groups = [...new Set(TOKENS.adjustables.map((f) => f.group))];
|
|
3077
3111
|
const rows = (group) => TOKENS.adjustables.filter((f) => f.group === group).map((f) => UI.html("adjust-row", { key: f.key, label: f.label, hint: f.hint, kind: f.kind, value: custom[f.key] || String(f.of(look)), own: String(f.of(look)), min: f.min, max: f.max, step: f.step })).join("");
|
|
3078
3112
|
return `<div class="look-preview" id="sp-look-preview">${lookSampleHtml()}</div>
|
|
3079
|
-
${field("Look", `<input type="hidden" id="sp-look" value="${esc(lookId)}"><div class="look-cards">${cards}</div>`, "How the room is drawn. Every element keeps what it does; only its look changes. The sample above wears the look you pick; the window changes on save.")}
|
|
3113
|
+
${field("Look", `<input type="hidden" id="sp-look" value="${esc(lookId)}"><div class="look-cards">${cards}</div><div class="look-own" id="sp-look-own">${UI.html("button", { label: "Import a look…", kind: "ghost", size: "xs", hook: "sp-look-import", title: "A look file (.json) saved from viberoom, yours or someone else's" })}<span class="own-only"${look.custom ? "" : " hidden"}>${UI.html("button", { label: "Export", kind: "ghost", size: "xs", hook: "sp-look-export", title: "Save this look as a file, to share or keep" })}${UI.html("button", { label: "Delete", kind: "danger", size: "xs", hook: "sp-look-delete", title: "Remove this look from your own looks" })}</span><input type="file" id="sp-look-file" accept=".json,application/json" hidden></div>`, "How the room is drawn. Every element keeps what it does; only its look changes. The sample above wears the look you pick; the window changes on save. Your own looks (a vibemate's, or a file you import) come after the ones viberoom ships.")}
|
|
3080
3114
|
<div class="look-adjust" id="sp-look-adjust">
|
|
3081
3115
|
<div class="adjust-head"><span class="label">Fine-tune <span id="sp-adj-name">${esc(look.label)}</span></span>${UI.html("button", { label: "Reset all", kind: "ghost", size: "xs", hook: "sp-adj-reset", title: "Back to the look as designed" })}</div>
|
|
3082
3116
|
<p class="hint">Kept for this look alone. The sample follows as you pick; the window follows on save.</p>
|
|
@@ -3151,6 +3185,7 @@
|
|
|
3151
3185
|
${field("Hop limit", `${UI.html("number-field", { id: "sp-hops", value: String(d.hopLimit), min: 0, max: 10000 })}`, "How many vibemate-to-vibemate replies may follow one message of yours before the room waits for you again.")}
|
|
3152
3186
|
${field("Full brief every N turns", `${UI.html("number-field", { id: "sp-brief-turns", value: String(d.fullBriefEveryTurns), min: 1, max: 10000 })}`, "How often a vibemate gets the whole room brief again instead of the short header.")}
|
|
3153
3187
|
${field("Full brief every N tokens", `${UI.html("number-field", { id: "sp-brief-tokens", value: String(d.fullBriefEveryTokens), min: 1000, max: 10000000, step: 1000 })}`, "…or after this much new context since its last full brief, whichever comes first.")}
|
|
3188
|
+
${field("Most characters in a vibio or in the room rules", `${UI.html("number-field", { id: "sp-text-limit", value: String(d.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`, "Both go into every brief. Over the limit, the text is refused with the numbers, never cut; each room can raise or lower its own.")}
|
|
3154
3189
|
<label class="switch"><span class="label">Repeat core rules in every header<span class="hint">The short header before each turn repeats the room's core rules (who is here, how to address, how long to write).</span></span><input type="checkbox" id="sp-header-rules" ${d.headerRules ? "checked" : ""}></label>
|
|
3155
3190
|
${field("Tools", `<select id="sp-tools"><option value="on-request"${d.tools === "on-request" ? " selected" : ""}>Only when asked</option><option value="never"${d.tools === "never" ? " selected" : ""}>Never</option></select>`, "Whether vibemates may use their own tools (files, shell, web) without being asked to.")}
|
|
3156
3191
|
</div>
|
|
@@ -3275,9 +3310,56 @@
|
|
|
3275
3310
|
input.value = b.dataset.look;
|
|
3276
3311
|
fillRows(b.dataset.look);
|
|
3277
3312
|
paintLookPreview();
|
|
3313
|
+
paintOwnRow();
|
|
3278
3314
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3279
3315
|
}),
|
|
3280
3316
|
);
|
|
3317
|
+
const paintOwnRow = () => {
|
|
3318
|
+
const own = looksBox.querySelector("#sp-look-own .own-only");
|
|
3319
|
+
if (own) own.hidden = !pickedLook().custom;
|
|
3320
|
+
};
|
|
3321
|
+
looksBox.querySelector(".sp-look-import").addEventListener("click", () => $("#sp-look-file").click());
|
|
3322
|
+
$("#sp-look-file").addEventListener("change", async () => {
|
|
3323
|
+
const file = $("#sp-look-file").files && $("#sp-look-file").files[0];
|
|
3324
|
+
$("#sp-look-file").value = "";
|
|
3325
|
+
if (!file) return;
|
|
3326
|
+
try {
|
|
3327
|
+
const spec = JSON.parse(await file.text());
|
|
3328
|
+
const checked = await post("/api/looks/check", { spec });
|
|
3329
|
+
if (!checked.ok) throw new Error(`${file.name} is not a look viberoom can wear: ${(checked.errors || []).map((x) => x.message).join("; ")}`);
|
|
3330
|
+
const taken = TOKENS.looks[checked.id];
|
|
3331
|
+
if (taken && !taken.custom) throw new Error(`"${checked.id}" is the id of a look viberoom ships; change the id in the file`);
|
|
3332
|
+
if (taken && !(await confirmDialog(`You have a look "${taken.label}" with this id already. Replace it with the one from ${file.name}?`, { title: "Replace the look?", okLabel: "Replace" }))) return;
|
|
3333
|
+
const saved = await post("/api/looks", { spec, replace: !!taken });
|
|
3334
|
+
toast(`Look "${saved.look.label}" ${taken ? "replaced" : "added"}: it is in the list now.${(saved.warnings || []).length ? ` ${saved.warnings.map((w) => w.message).join(" ")}` : ""}`, "success");
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
showError(error);
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
looksBox.querySelector(".sp-look-export").addEventListener("click", () => {
|
|
3340
|
+
const look = pickedLook();
|
|
3341
|
+
const spec = state.looks.find((l) => l.id === look.id);
|
|
3342
|
+
if (!spec) return;
|
|
3343
|
+
const a = document.createElement("a");
|
|
3344
|
+
a.href = URL.createObjectURL(new Blob([`${JSON.stringify(spec, null, 2)}\n`], { type: "application/json" }));
|
|
3345
|
+
a.download = `${look.id}.json`;
|
|
3346
|
+
document.body.appendChild(a);
|
|
3347
|
+
a.click();
|
|
3348
|
+
a.remove();
|
|
3349
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
|
3350
|
+
});
|
|
3351
|
+
looksBox.querySelector(".sp-look-delete").addEventListener("click", async () => {
|
|
3352
|
+
const look = pickedLook();
|
|
3353
|
+
if (!look.custom) return;
|
|
3354
|
+
const ok = await confirmDialog(`"${look.label}" goes from your looks; a window wearing it falls back to VibeClassic. The file is gone too (export it first to keep it).`, { title: "Delete the look?", okLabel: "Delete", danger: true });
|
|
3355
|
+
if (!ok) return;
|
|
3356
|
+
try {
|
|
3357
|
+
await post("/api/looks/remove", { id: look.id });
|
|
3358
|
+
toast(`Look "${look.label}" deleted.`, "success");
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
showError(error);
|
|
3361
|
+
}
|
|
3362
|
+
});
|
|
3281
3363
|
looksBox.querySelectorAll('[data-ui="adjust-row"] input').forEach((i) => i.addEventListener("input", paintLookPreview));
|
|
3282
3364
|
looksBox.addEventListener("click", (e) => {
|
|
3283
3365
|
const back = e.target.closest && e.target.closest('[data-ui="adjust-row"] .back');
|
|
@@ -3359,6 +3441,7 @@
|
|
|
3359
3441
|
hopLimit: Number($("#sp-hops").value),
|
|
3360
3442
|
fullBriefEveryTurns: Number($("#sp-brief-turns").value),
|
|
3361
3443
|
fullBriefEveryTokens: Number($("#sp-brief-tokens").value),
|
|
3444
|
+
briefTextLimit: Number($("#sp-text-limit").value),
|
|
3362
3445
|
headerRules: $("#sp-header-rules").checked,
|
|
3363
3446
|
tools: $("#sp-tools").value,
|
|
3364
3447
|
},
|
|
@@ -3695,7 +3778,7 @@
|
|
|
3695
3778
|
}
|
|
3696
3779
|
els.invModelCustom.hidden = !info.modelAtLaunch;
|
|
3697
3780
|
const who = info.agentInfo && info.agentInfo.name ? `${info.agentInfo.name} ${info.agentInfo.version || ""}`.trim() : recipe.vendor;
|
|
3698
|
-
els.invStatus.textContent = parts.length ? `Options from ${who} (${parts.join(", ")}; ${(info.durationMs / 1000).toFixed(1)} s)` : `${who} exposes no config options over ACP${info.modelAtLaunch ? "; the model is a launch flag (built-in list, or type one)" : ""}.`;
|
|
3781
|
+
els.invStatus.textContent = parts.length ? `Options from ${who} (${parts.join(", ")}; ${(info.durationMs / 1000).toFixed(1)} s)` : `${who} exposes no config options over ACP${info.modelAtLaunch ? "; the model is a launch flag (built-in list, or type one)" : ""}${info.modeAtLaunch ? "; the mode is a launch flag (a change restarts the session)" : ""}.`;
|
|
3699
3782
|
} catch (error) {
|
|
3700
3783
|
if (requestId !== optionsRequest) return;
|
|
3701
3784
|
els.invStatus.textContent = `Could not read the vibemate's options (${error.message}); showing the built-in list.`;
|
|
@@ -4066,6 +4149,7 @@
|
|
|
4066
4149
|
${stpField("…or every N tokens", `${UI.html("number-field", { id: "stp-brief-tokens", value: String(st.fullBriefEveryTokens ?? 20000), min: 1000, max: 10000000, step: 1000 })}`)}
|
|
4067
4150
|
${stpField("Replay after a reconnect", `${UI.html("number-field", { id: "stp-replay", value: String(st.replayAfterRestart ?? 10), min: 0, max: 200 })}`)}
|
|
4068
4151
|
${stpField("Missed messages read at most", `${UI.html("number-field", { id: "stp-backlog", value: String(st.backlogCap ?? 50), min: 1, max: 1000 })}`)}
|
|
4152
|
+
${stpField("Most characters in a vibio or the rules", `${UI.html("number-field", { id: "stp-text-limit", value: String(st.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`)}
|
|
4069
4153
|
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
4070
4154
|
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
4071
4155
|
</div>`;
|
|
@@ -4087,7 +4171,7 @@
|
|
|
4087
4171
|
</div></div>`).join("");
|
|
4088
4172
|
return `
|
|
4089
4173
|
<div class="stp-section"><h5>Room</h5>${room}</div>
|
|
4090
|
-
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5"
|
|
4174
|
+
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5" placeholder="one rule per line">${esc(st.customRules || "")}</textarea></div>
|
|
4091
4175
|
<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">${UI.html("button", { label: "Browse", icon: "folder", kind: "ghost", hook: "browse-btn", data: { stpBrowse: true } })}</span></div>
|
|
4092
4176
|
<div class="stp-section"><h5>Vibemates · <span id="stp-vm-count">${(t.vibemates || []).length}</span></h5>${vms || '<span class="hint">none</span>'}</div>`;
|
|
4093
4177
|
}
|
|
@@ -4112,7 +4196,8 @@
|
|
|
4112
4196
|
backlogCap: num("#stp-backlog"),
|
|
4113
4197
|
headerRules: $("#stp-header-rules").checked,
|
|
4114
4198
|
showVendorInRoster: $("#stp-vendor").checked,
|
|
4115
|
-
customRules: $("#stp-rules").value
|
|
4199
|
+
customRules: $("#stp-rules").value,
|
|
4200
|
+
briefTextLimit: num("#stp-text-limit"),
|
|
4116
4201
|
};
|
|
4117
4202
|
const vibemates = [...stpEls.preview.querySelectorAll(".stp-vm")].map((row) => {
|
|
4118
4203
|
const v = {};
|
|
@@ -4199,7 +4284,7 @@
|
|
|
4199
4284
|
els.pfDialog.addEventListener("cancel", (event) => event.preventDefault());
|
|
4200
4285
|
function offerLook() {
|
|
4201
4286
|
const current = (state.settings.appearance || {}).look || TOKENS.current.id;
|
|
4202
|
-
$("#look-dialog-cards").innerHTML = Object.values(TOKENS.looks).map((l) => UI.html("look-card", { look: l, on: l.id === current, data: { look: l.id } })).join("");
|
|
4287
|
+
$("#look-dialog-cards").innerHTML = Object.values(TOKENS.looks).map((l) => UI.html("look-card", { look: l, tag: lookTag(l), on: l.id === current, data: { look: l.id } })).join("");
|
|
4203
4288
|
openDialog($("#look-dialog"));
|
|
4204
4289
|
}
|
|
4205
4290
|
$("#look-dialog-cards").addEventListener("click", async (e) => {
|
|
@@ -4274,10 +4359,47 @@
|
|
|
4274
4359
|
for (const name of CUSTOM_VARS) el.style.removeProperty(name);
|
|
4275
4360
|
for (const [name, value] of Object.entries(customVars(values || {}))) el.style.setProperty(name, value);
|
|
4276
4361
|
}
|
|
4362
|
+
function registerLooks(specs) {
|
|
4363
|
+
for (const id of Object.keys(TOKENS.looks)) if (TOKENS.looks[id].custom) delete TOKENS.looks[id];
|
|
4364
|
+
for (const spec of specs || []) {
|
|
4365
|
+
try {
|
|
4366
|
+
TOKENS.looks[spec.id] = TOKENS.make(spec);
|
|
4367
|
+
} catch (error) {
|
|
4368
|
+
console.warn(`look ${spec.id} could not be built: ${error.message}`);
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
function refreshLooksCss() {
|
|
4373
|
+
const link = $("#looks-custom");
|
|
4374
|
+
if (link) link.href = `/looks-custom.css?v=${Date.now()}`;
|
|
4375
|
+
}
|
|
4376
|
+
function lookTag(look) {
|
|
4377
|
+
return look.custom ? `${(state.settings && state.settings.humanName) || "Your"}'s look` : undefined;
|
|
4378
|
+
}
|
|
4379
|
+
function tryLook(id) {
|
|
4380
|
+
const root = document.documentElement;
|
|
4381
|
+
if (root.dataset.tried === id || !TOKENS.looks[id]) {
|
|
4382
|
+
applyAppearance();
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
if (id === TOKENS.current.id) delete root.dataset.look;
|
|
4386
|
+
else root.dataset.look = id;
|
|
4387
|
+
applyCustomVars(root, (((state.settings || {}).appearance || {}).custom || {})[id] || {});
|
|
4388
|
+
root.dataset.tried = id;
|
|
4389
|
+
refreshTryButtons();
|
|
4390
|
+
}
|
|
4391
|
+
function refreshTryButtons() {
|
|
4392
|
+
const tried = document.documentElement.dataset.tried || "";
|
|
4393
|
+
document.querySelectorAll('[data-ui="ask-card"] [data-act="try-look"]').forEach((b) => {
|
|
4394
|
+
const label = b.querySelector(".label") || b;
|
|
4395
|
+
label.textContent = tried === b.dataset.look ? "Back" : "Try it on";
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4277
4398
|
let appliedScheme = null;
|
|
4278
4399
|
function applyAppearance() {
|
|
4279
4400
|
const a = (state.settings || {}).appearance || {};
|
|
4280
4401
|
const root = document.documentElement;
|
|
4402
|
+
delete root.dataset.tried;
|
|
4281
4403
|
root.style.setProperty("--fs-scale", String((a.chatFontSize || 14.5) / 14.5));
|
|
4282
4404
|
const look = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
4283
4405
|
if (look === TOKENS.current.id) delete root.dataset.look;
|
|
@@ -4295,6 +4417,7 @@
|
|
|
4295
4417
|
if (frame) frame.content = TOKENS.looks[look].elements.browser.themeColor;
|
|
4296
4418
|
applyCustomVars(root, (a.custom || {})[look] || {});
|
|
4297
4419
|
document.querySelectorAll("#participants .avatar .life").forEach((ring) => setLifePath(ring, ring.parentElement));
|
|
4420
|
+
refreshTryButtons();
|
|
4298
4421
|
}
|
|
4299
4422
|
|
|
4300
4423
|
let hubIdentity = null;
|
|
@@ -4307,6 +4430,8 @@
|
|
|
4307
4430
|
}
|
|
4308
4431
|
if (identity) hubIdentity = identity;
|
|
4309
4432
|
state.settings = snapshot.settings;
|
|
4433
|
+
state.looks = snapshot.looks || [];
|
|
4434
|
+
registerLooks(state.looks);
|
|
4310
4435
|
applyAppearance();
|
|
4311
4436
|
state.update = snapshot.update || null;
|
|
4312
4437
|
renderUpdatePop();
|
|
@@ -4485,6 +4610,13 @@
|
|
|
4485
4610
|
if (state.view === "skills" && !editingInDetails()) renderSkillsPage();
|
|
4486
4611
|
if (state.detailsOpen && !editingInDetails()) renderDetails();
|
|
4487
4612
|
},
|
|
4613
|
+
looks: (m) => {
|
|
4614
|
+
state.looks = m.looks || [];
|
|
4615
|
+
registerLooks(state.looks);
|
|
4616
|
+
refreshLooksCss();
|
|
4617
|
+
applyAppearance();
|
|
4618
|
+
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
4619
|
+
},
|
|
4488
4620
|
settings: (m) => {
|
|
4489
4621
|
state.settings = m.settings;
|
|
4490
4622
|
applyAppearance();
|