viberoom 0.6.0 → 0.7.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 +14 -0
- package/dist/hub.js +45 -4
- package/dist/main.js +1 -1
- package/dist/recipes.js +2 -1
- package/dist/shortcuts.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +131 -107
- package/ui/app.js +334 -87
- package/ui/avatars.js +7 -14
- package/ui/components.css +116 -21
- package/ui/components.js +202 -1
- package/ui/fonts/anonymous-pro-cyrillic-400.woff2 +0 -0
- package/ui/fonts/anonymous-pro-cyrillic-700.woff2 +0 -0
- package/ui/fonts/anonymous-pro-latin-400.woff2 +0 -0
- package/ui/fonts/anonymous-pro-latin-700.woff2 +0 -0
- package/ui/fonts/anonymous-pro-latin-ext-400.woff2 +0 -0
- package/ui/fonts/anonymous-pro-latin-ext-700.woff2 +0 -0
- package/ui/fonts/anonymous-pro.css +7 -0
- package/ui/fonts/cascadia-code-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/cascadia-code-cyrillic.woff2 +0 -0
- package/ui/fonts/cascadia-code-latin-ext.woff2 +0 -0
- package/ui/fonts/cascadia-code-latin.woff2 +0 -0
- package/ui/fonts/cascadia-code.css +5 -0
- package/ui/fonts/comfortaa-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/comfortaa-cyrillic.woff2 +0 -0
- package/ui/fonts/comfortaa-latin-ext.woff2 +0 -0
- package/ui/fonts/comfortaa-latin.woff2 +0 -0
- package/ui/fonts/comfortaa.css +5 -0
- package/ui/fonts/exo-2-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/exo-2-cyrillic.woff2 +0 -0
- package/ui/fonts/exo-2-latin-ext.woff2 +0 -0
- package/ui/fonts/exo-2-latin.woff2 +0 -0
- package/ui/fonts/exo-2.css +5 -0
- package/ui/fonts/golos-text-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/golos-text-cyrillic.woff2 +0 -0
- package/ui/fonts/golos-text-latin-ext.woff2 +0 -0
- package/ui/fonts/golos-text-latin.woff2 +0 -0
- package/ui/fonts/golos-text.css +5 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-cyrillic-ext-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono-latin-ext-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-mono.css +13 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-ext-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-ext-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-cyrillic-ext-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-ext-400.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-ext-600.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans-latin-ext-700.woff2 +0 -0
- package/ui/fonts/ibm-plex-sans.css +13 -0
- package/ui/fonts/manrope-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/manrope-cyrillic.woff2 +0 -0
- package/ui/fonts/manrope-latin-ext.woff2 +0 -0
- package/ui/fonts/manrope-latin.woff2 +0 -0
- package/ui/fonts/manrope.css +5 -0
- package/ui/fonts/montserrat-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/montserrat-cyrillic.woff2 +0 -0
- package/ui/fonts/montserrat-latin-ext.woff2 +0 -0
- package/ui/fonts/montserrat-latin.woff2 +0 -0
- package/ui/fonts/montserrat.css +5 -0
- package/ui/fonts/open-sans-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/open-sans-cyrillic.woff2 +0 -0
- package/ui/fonts/open-sans-latin-ext.woff2 +0 -0
- package/ui/fonts/open-sans-latin.woff2 +0 -0
- package/ui/fonts/open-sans.css +5 -0
- package/ui/fonts/pt-mono-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/pt-mono-cyrillic.woff2 +0 -0
- package/ui/fonts/pt-mono-latin-ext.woff2 +0 -0
- package/ui/fonts/pt-mono-latin.woff2 +0 -0
- package/ui/fonts/pt-mono.css +5 -0
- package/ui/fonts/rubik-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/rubik-cyrillic.woff2 +0 -0
- package/ui/fonts/rubik-latin-ext.woff2 +0 -0
- package/ui/fonts/rubik-latin.woff2 +0 -0
- package/ui/fonts/rubik.css +5 -0
- package/ui/fonts/source-sans-3-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/source-sans-3-cyrillic.woff2 +0 -0
- package/ui/fonts/source-sans-3-latin-ext.woff2 +0 -0
- package/ui/fonts/source-sans-3-latin.woff2 +0 -0
- package/ui/fonts/source-sans-3.css +5 -0
- package/ui/fonts/ubuntu-sans-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/ubuntu-sans-cyrillic.woff2 +0 -0
- package/ui/fonts/ubuntu-sans-latin-ext.woff2 +0 -0
- package/ui/fonts/ubuntu-sans-latin.woff2 +0 -0
- package/ui/fonts/ubuntu-sans.css +5 -0
- package/ui/fonts/victor-mono-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/victor-mono-cyrillic.woff2 +0 -0
- package/ui/fonts/victor-mono-latin-ext.woff2 +0 -0
- package/ui/fonts/victor-mono-latin.woff2 +0 -0
- package/ui/fonts/victor-mono.css +5 -0
- package/ui/index.html +15 -6
- package/ui/looks.css +2446 -0
- package/ui/manifest.json +3 -3
- package/ui/theme.css +114 -84
- package/ui/tokens.js +315 -29
- package/ui/ui.js +2 -0
package/ui/app.js
CHANGED
|
@@ -166,6 +166,16 @@
|
|
|
166
166
|
nunito: { label: "Nunito (default)", stack: '"Nunito", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
167
167
|
inter: { label: "Inter", stack: '"Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
168
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' },
|
|
169
179
|
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
170
180
|
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
171
181
|
},
|
|
@@ -173,6 +183,11 @@
|
|
|
173
183
|
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
174
184
|
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
175
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' },
|
|
176
191
|
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
177
192
|
},
|
|
178
193
|
};
|
|
@@ -180,7 +195,7 @@
|
|
|
180
195
|
const STATUS_TONE = { idle: "ready", queued: "waiting", starting: "waiting", thinking: "thinking", writing: "writing", error: "error", offline: "asleep", left: "asleep", unstaffed: "attention" };
|
|
181
196
|
const TOOL_STATUSES = new Set(["pending", "in_progress", "completed", "failed"]);
|
|
182
197
|
const TOKENS = globalThis.VIBEROOM_TOKENS;
|
|
183
|
-
const FALLBACK_COLOR = TOKENS.
|
|
198
|
+
const FALLBACK_COLOR = () => TOKENS.active().elements.face.fallback;
|
|
184
199
|
const WORKING_SVG = '<svg class="working" viewBox="0 0 44 35" aria-hidden="true" title="working…">'
|
|
185
200
|
+ '<g class="body">'
|
|
186
201
|
+ '<circle cx="20" cy="6.5" r="5.6" fill="currentColor"/>'
|
|
@@ -825,9 +840,13 @@
|
|
|
825
840
|
function diagramSettings(d) {
|
|
826
841
|
return d || (state.settings && state.settings.diagrams) || {};
|
|
827
842
|
}
|
|
843
|
+
function diagramPreset(d) {
|
|
844
|
+
const look = TOKENS.active();
|
|
845
|
+
return TOKENS.diagrams.forScheme(DIAGRAM_PRESETS[d.preset] || DIAGRAM_PRESETS.pop, look.scheme, look.elements.diagram.nodeInk);
|
|
846
|
+
}
|
|
828
847
|
function mermaidThemeVariables(d) {
|
|
829
848
|
d = diagramSettings(d);
|
|
830
|
-
const preset =
|
|
849
|
+
const preset = diagramPreset(d);
|
|
831
850
|
const vars = Object.assign({}, preset, { fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font").trim() || "Nunito, sans-serif", fontSize: "13px" });
|
|
832
851
|
delete vars.label;
|
|
833
852
|
delete vars.palette;
|
|
@@ -841,19 +860,19 @@
|
|
|
841
860
|
}
|
|
842
861
|
function diagramPalette(d) {
|
|
843
862
|
d = diagramSettings(d);
|
|
844
|
-
const preset =
|
|
863
|
+
const preset = diagramPreset(d);
|
|
845
864
|
return preset.palette && !d.primary ? preset.palette : null;
|
|
846
865
|
}
|
|
847
|
-
const
|
|
866
|
+
const mermaidCss = () => [
|
|
848
867
|
".node rect, .node .label-container, .node .basic, .cluster rect, rect.actor { rx: 12px; ry: 12px; }",
|
|
849
|
-
`.node .label-container, .node .basic, .node rect, .node circle, .node ellipse, rect.actor { stroke-width: 1.8px; filter: drop-shadow(0 2px 0 ${TOKENS.
|
|
868
|
+
`.node .label-container, .node .basic, .node rect, .node circle, .node ellipse, rect.actor { stroke-width: 1.8px; filter: drop-shadow(0 2px 0 ${TOKENS.active().elements.diagram.nodeShadow}); }`,
|
|
850
869
|
".edgePath .path, .flowchart-link, .messageLine0, .messageLine1, .transition, .relation { stroke-width: 2px; }",
|
|
851
870
|
".edgeLabel, .edgeLabel p { font-weight: 700; }",
|
|
852
871
|
".cluster rect { stroke-dasharray: 4 3; stroke-width: 1.5px; }",
|
|
853
872
|
".cluster-label, .cluster-label p { font-weight: 800; }",
|
|
854
873
|
].join(" ");
|
|
855
874
|
function paintDiagram(root, palette) {
|
|
856
|
-
const ink = TOKENS.
|
|
875
|
+
const ink = TOKENS.active().elements.diagram.nodeInk;
|
|
857
876
|
const byKey = new Map();
|
|
858
877
|
const pick = (key) => {
|
|
859
878
|
if (!byKey.has(key)) byKey.set(key, palette[byKey.size % palette.length]);
|
|
@@ -911,7 +930,7 @@
|
|
|
911
930
|
theme: "base",
|
|
912
931
|
securityLevel: "strict",
|
|
913
932
|
themeVariables: mermaidThemeVariables(d),
|
|
914
|
-
themeCSS:
|
|
933
|
+
themeCSS: mermaidCss(),
|
|
915
934
|
flowchart: { curve: "basis", padding: 14, nodeSpacing: 44, rankSpacing: 52 },
|
|
916
935
|
});
|
|
917
936
|
for (const block of blocks) {
|
|
@@ -993,7 +1012,7 @@
|
|
|
993
1012
|
}
|
|
994
1013
|
function meAvatarData() {
|
|
995
1014
|
const s = state.settings || {};
|
|
996
|
-
return { name: s.humanName || "You", color: TOKENS.
|
|
1015
|
+
return { name: s.humanName || "You", color: TOKENS.active().elements.face.humanInk, avatar: s.humanAvatar, kind: "human" };
|
|
997
1016
|
}
|
|
998
1017
|
function copyableHtml(el) {
|
|
999
1018
|
const clone = (el.querySelector(".text > .words") || el.querySelector(".text")).cloneNode(true);
|
|
@@ -1044,11 +1063,26 @@
|
|
|
1044
1063
|
t.hidden = !t.hidden;
|
|
1045
1064
|
b.classList.toggle("on", !t.hidden);
|
|
1046
1065
|
});
|
|
1066
|
+
document.addEventListener("click", (e) => {
|
|
1067
|
+
const step = e.target.closest && e.target.closest('[data-ui="number-field"] > .steps > button');
|
|
1068
|
+
if (!step) return;
|
|
1069
|
+
e.preventDefault();
|
|
1070
|
+
const input = step.closest('[data-ui="number-field"]').querySelector("input");
|
|
1071
|
+
if (!input || input.disabled) return;
|
|
1072
|
+
try {
|
|
1073
|
+
if (step.dataset.act === "up") input.stepUp();
|
|
1074
|
+
else input.stepDown();
|
|
1075
|
+
} catch {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1079
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1080
|
+
});
|
|
1047
1081
|
function field(label, inputHtml, hint, geekText) {
|
|
1048
1082
|
return `<label class="field"><span class="label">${label}${geekText ? geekTip(geekText) : ""}</span>${inputHtml}${hint ? `<span class="hint">${hint}</span>` : ""}</label>`;
|
|
1049
1083
|
}
|
|
1050
|
-
function vendorLogo(r) {
|
|
1051
|
-
return
|
|
1084
|
+
function vendorLogo(r, size) {
|
|
1085
|
+
return UI.html("logo-tile", { icon: r.icon || "", letter: r.vendor[0], size: size || "md", title: r.vendor });
|
|
1052
1086
|
}
|
|
1053
1087
|
function slowTasksHtml() {
|
|
1054
1088
|
if (!slowTasks.length) return '<p class="hint">Nothing over 8 ms so far in this window.</p>';
|
|
@@ -1146,11 +1180,18 @@
|
|
|
1146
1180
|
return h % 360;
|
|
1147
1181
|
}
|
|
1148
1182
|
function roomMark(room, lg) {
|
|
1149
|
-
const hue = roomHue(room);
|
|
1150
1183
|
const emoji = (room.settings && room.settings.emoji) || "";
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1184
|
+
return UI.html("room-mark", { hue: roomHue(room), emoji: emoji || undefined, letter: emoji ? undefined : room.name.trim()[0] || "?", size: lg ? "lg" : "md" });
|
|
1185
|
+
}
|
|
1186
|
+
function lookSampleHtml() {
|
|
1187
|
+
const withIcon = state.recipes.filter((r) => r.icon);
|
|
1188
|
+
const lead = withIcon[0] || state.recipes[0];
|
|
1189
|
+
const sam = { name: "Sam", kind: "agent", color: TOKENS.active().elements.face.avatarDefault, avatar: "🦊", agentType: lead ? lead.id : "", agentLabel: lead ? lead.vendor : "" };
|
|
1190
|
+
const marks = (withIcon.length ? withIcon : state.recipes).slice(0, 3).map((r) => vendorLogo(r, "sm")).join("");
|
|
1191
|
+
return `<div class="msg agent"><div class="bubble-col"><div class="head"><span class="head-av">${avatar(sam, 32, { vendor: true })}</span><span class="name" style="color:${sam.color}">Sam</span><span class="time">16:21</span></div><div class="bubble">A reply, on this look's paper, in its ink, with <code>code</code> a step smaller.</div><div class="meta"><span>seen</span></div></div></div>
|
|
1192
|
+
<div class="msg mine"><div class="bubble-col"><div class="bubble">Your message.</div></div></div>
|
|
1193
|
+
<div class="sample-row">${UI.html("hub-row", { text: "Sam finished the reply started at 16:21 · 2m 30s", ref: "sample", title: "a row the hub writes", face: UI.raw(avatar(sam, 20, {})) })}</div>
|
|
1194
|
+
<div class="sample-row">${UI.html("room-mark", { hue: 200, letter: "V", title: "a room" })}${UI.html("room-mark", { hue: 40, emoji: "🎭", title: "a room with an emoji" })}${marks}${UI.html("button", { label: "Send", kind: "primary", size: "sm" })}</div>`;
|
|
1154
1195
|
}
|
|
1155
1196
|
function roomTitle(room) {
|
|
1156
1197
|
const emoji = (room.settings && room.settings.emoji) || "";
|
|
@@ -1168,10 +1209,11 @@
|
|
|
1168
1209
|
|
|
1169
1210
|
function attachScrollHints(box) {
|
|
1170
1211
|
if (!box || box.dataset.hinted) return;
|
|
1212
|
+
const inside = box.tagName === "DIALOG";
|
|
1171
1213
|
const frame = box.parentElement;
|
|
1172
|
-
if (!frame) return;
|
|
1214
|
+
if (!frame && !inside) return;
|
|
1173
1215
|
box.dataset.hinted = "1";
|
|
1174
|
-
if (getComputedStyle(frame).position === "static") frame.style.position = "relative";
|
|
1216
|
+
if (!inside && getComputedStyle(frame).position === "static") frame.style.position = "relative";
|
|
1175
1217
|
const make = (dir) => {
|
|
1176
1218
|
const b = document.createElement("button");
|
|
1177
1219
|
b.type = "button";
|
|
@@ -1180,7 +1222,13 @@
|
|
|
1180
1222
|
b.innerHTML = ic(dir === "up" ? "chevrons-up" : "chevrons-down");
|
|
1181
1223
|
b.hidden = true;
|
|
1182
1224
|
b.addEventListener("click", () => box.scrollBy({ top: (dir === "up" ? -1 : 1) * Math.max(120, box.clientHeight * 0.85), behavior: "smooth" }));
|
|
1183
|
-
|
|
1225
|
+
if (inside) {
|
|
1226
|
+
const slot = document.createElement("div");
|
|
1227
|
+
slot.className = `scroll-hint-slot ${dir}`;
|
|
1228
|
+
slot.appendChild(b);
|
|
1229
|
+
if (dir === "up") box.prepend(slot);
|
|
1230
|
+
else box.append(slot);
|
|
1231
|
+
} else frame.appendChild(b);
|
|
1184
1232
|
return b;
|
|
1185
1233
|
};
|
|
1186
1234
|
const up = make("up");
|
|
@@ -1427,7 +1475,7 @@
|
|
|
1427
1475
|
renderRailRooms();
|
|
1428
1476
|
const s = state.settings;
|
|
1429
1477
|
if (s) {
|
|
1430
|
-
els.railMeAvatar.innerHTML = avatar(meAvatarData(), 44, {});
|
|
1478
|
+
els.railMeAvatar.innerHTML = avatar(meAvatarData(), 44, { kind: "bare" });
|
|
1431
1479
|
els.railMeLabel.textContent = s.humanName;
|
|
1432
1480
|
}
|
|
1433
1481
|
const open = els.app.classList.contains("rail-open");
|
|
@@ -1510,7 +1558,7 @@
|
|
|
1510
1558
|
const q = state.roomSearch.toLowerCase();
|
|
1511
1559
|
const all = document.createElement("li");
|
|
1512
1560
|
all.className = state.skillsRoom ? "" : "selected";
|
|
1513
|
-
all.innerHTML =
|
|
1561
|
+
all.innerHTML = `${UI.html("room-mark", { icon: "skills", title: "All skills" })}<div class="p-body"><div class="p-name"><span>All skills</span></div><div class="p-preview">${(state.skills || []).length} in the library</div></div>`;
|
|
1514
1562
|
all.addEventListener("click", () => {
|
|
1515
1563
|
state.skillsRoom = null;
|
|
1516
1564
|
renderSideSkills();
|
|
@@ -1568,6 +1616,7 @@
|
|
|
1568
1616
|
{ tone: "peach", emoji: "🧩", title: "Skills", text: "Reusable instructions in your library. Attach them to vibemates, invoke one with /name, or let a vibemate write its own." },
|
|
1569
1617
|
{ tone: "rose", emoji: "🤫", title: "Hush", text: "Too much at once? One click stops every running reply. The vibemates stay quiet until you write again." },
|
|
1570
1618
|
{ tone: "sky", emoji: "📊", title: "Links and diagrams", text: "Links and file paths open on your machine with one click. A ```mermaid block in a reply turns into a diagram." },
|
|
1619
|
+
{ tone: "orchid", emoji: "🎨", title: "Looks", text: "VibeClassic and its dark twin, Terminal, Clay, Eye Comfort (the look the research on tired eyes asks for) or 3D clayful (soft clay objects, with weight): pick one with the button above or under Settings → Appearance, and fine-tune its paper, bubbles, ink and corners." },
|
|
1571
1620
|
];
|
|
1572
1621
|
const STEPS = [
|
|
1573
1622
|
{ n: 1, title: "Open a room", text: "Give it a name and a folder. The folder is where the vibemates read and write." },
|
|
@@ -1579,7 +1628,7 @@
|
|
|
1579
1628
|
const name = s.humanName || "there";
|
|
1580
1629
|
const rooms = sortedRooms().slice(0, 4);
|
|
1581
1630
|
const vendors = state.recipes.filter((r) => !r.unavailableReason);
|
|
1582
|
-
const vendorLogos = (vendors.length ? vendors : state.recipes).map((r) => `<span class="home-vendor" title="${esc(r.vendor)}${r.unavailableReason ? " (not installed)" : ""}"${r.unavailableReason ? ' style="opacity:.45"' : ""}>${vendorLogo(r)}</span>`).join("");
|
|
1631
|
+
const vendorLogos = (vendors.length ? vendors : state.recipes).map((r) => `<span class="home-vendor" title="${esc(r.vendor)}${r.unavailableReason ? " (not installed)" : ""}"${r.unavailableReason ? ' style="opacity:.45"' : ""}>${vendorLogo(r, "sm")}</span>`).join("");
|
|
1583
1632
|
els.homeView.innerHTML = `
|
|
1584
1633
|
<div class="home-inner">
|
|
1585
1634
|
<section class="hero">
|
|
@@ -1589,6 +1638,7 @@
|
|
|
1589
1638
|
<p>One chat, many coding agents. Summon your vibemates into a room, talk to all of them at once, and let them talk to each other.</p>
|
|
1590
1639
|
<div class="hero-actions">
|
|
1591
1640
|
${UI.html("button", { label: "Open room", icon: "rooms", size: "cta", id: "home-open-room", hook: "hero-cta" })}
|
|
1641
|
+
${UI.html("button", { label: "Change the look", icon: "eye", size: "cta", id: "home-change-look", hook: "hero-cta", title: "Settings → Appearance: the looks and their fine-tuning" })}
|
|
1592
1642
|
</div>
|
|
1593
1643
|
</div>
|
|
1594
1644
|
<div class="hero-art" aria-hidden="true">
|
|
@@ -1608,7 +1658,7 @@
|
|
|
1608
1658
|
return `<button class="home-room" data-room="${esc(room.id)}">${roomMark(room)}<span class="hr-body"><span class="hr-name">${esc(room.name)}</span><span class="hr-sub">${esc(last ? `${last.fromName}: ${String(last.text).replace(/\s+/g, " ").slice(0, 70)}` : room.settings.topic || `${st.agents.length} vibemate${st.agents.length === 1 ? "" : "s"}`)}</span></span>${st.unread ? `<span class="count-pill">${st.unread}</span>` : `<span class="hr-time">${esc(relTime(st.last))}</span>`}</button>`;
|
|
1609
1659
|
})
|
|
1610
1660
|
.join("")}
|
|
1611
|
-
${rooms.length ? "" : `<div class="home-room empty-room"
|
|
1661
|
+
${rooms.length ? "" : `<div class="home-room empty-room">${UI.html("room-mark", { icon: "plus" })}<span class="hr-body"><span class="hr-name">No rooms yet</span><span class="hr-sub">Open one, summon a vibemate, say hello.</span></span></div>`}
|
|
1612
1662
|
</div>
|
|
1613
1663
|
</section>
|
|
1614
1664
|
<section class="home-section">
|
|
@@ -1640,6 +1690,15 @@
|
|
|
1640
1690
|
setView("rooms");
|
|
1641
1691
|
remember("view", "rooms");
|
|
1642
1692
|
});
|
|
1693
|
+
const changeLook = $("#home-change-look");
|
|
1694
|
+
if (changeLook) changeLook.addEventListener("click", () => {
|
|
1695
|
+
setView("settings");
|
|
1696
|
+
remember("view", "settings");
|
|
1697
|
+
requestAnimationFrame(() => {
|
|
1698
|
+
const section = $("#sp-appearance");
|
|
1699
|
+
if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1700
|
+
});
|
|
1701
|
+
});
|
|
1643
1702
|
const all = $("#home-all-rooms");
|
|
1644
1703
|
if (all) all.addEventListener("click", () => setView("rooms"));
|
|
1645
1704
|
els.homeView.querySelectorAll(".home-room[data-room]").forEach((b) => b.addEventListener("click", () => selectRoom(b.dataset.room)));
|
|
@@ -1666,7 +1725,7 @@
|
|
|
1666
1725
|
const st = roomStats(room);
|
|
1667
1726
|
const card = document.createElement("div");
|
|
1668
1727
|
card.className = `card hover room-card${room.id === state.currentRoomId ? " current" : ""}`;
|
|
1669
|
-
const faces = st.agents.slice(0, 5).map((p) => avatar(p, 26, { vendor: true })).join("");
|
|
1728
|
+
const faces = st.agents.slice(0, 5).map((p) => avatar(p, 26, { vendor: true, ring: true })).join("");
|
|
1670
1729
|
card.innerHTML = `
|
|
1671
1730
|
<div class="rc-head"><div class="rc-title">${roomMark(room)}<h3>${esc(room.name)}</h3></div>${st.unread ? `<span class="count-pill">${st.unread}</span>` : st.thinking ? '<span class="live-dot" title="a vibemate is replying"></span>' : ""}</div>
|
|
1672
1731
|
<div class="rc-topic">${esc(room.settings.topic || (st.chats.length ? String(st.chats[st.chats.length - 1].text).slice(0, 140) : "Nothing said yet."))}</div>
|
|
@@ -1706,8 +1765,8 @@
|
|
|
1706
1765
|
: asleep
|
|
1707
1766
|
? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
|
|
1708
1767
|
: p.kind === "agent" && p.status !== "idle" ? UI.html("badge", { label: STATUS_LABEL[shown] || shown, tone: STATUS_TONE[shown] || "plain", dot: p.status === "thinking" }) : "";
|
|
1709
|
-
const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true, muted: p.muted });
|
|
1710
|
-
const
|
|
1768
|
+
const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true, muted: p.muted, me: p.kind === "human", dim: unstaffed ? "unstaffed" : asleep ? "asleep" : undefined });
|
|
1769
|
+
const statusValue = p.kind === "agent" ? shown || "idle" : "";
|
|
1711
1770
|
const bodyHtml = `<div class="p-body">
|
|
1712
1771
|
<div class="p-name"><span>${esc(p.name)}</span>${p.muted ? UI.html("badge", { label: "muted", tone: "muted" }) : ""}${status}</div>
|
|
1713
1772
|
<div class="p-sub">${esc(sub)}</div>
|
|
@@ -1721,16 +1780,16 @@
|
|
|
1721
1780
|
li.dataset.id = p.id;
|
|
1722
1781
|
li.innerHTML = avatarHtml + bodyHtml + (p.kind === "agent" ? UI.html("row-button", { icon: "settings", title: `Open ${p.name}'s panel`, act: "panel" }) + UI.html("row-button", { icon: "last-reply", title: `Go to ${p.name}'s last reply`, act: "last-reply" }) : "");
|
|
1723
1782
|
li.dataset.avatar = avatarHtml;
|
|
1724
|
-
if (
|
|
1783
|
+
if (statusValue) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="status" data-status="${esc(statusValue)}"></span>`);
|
|
1725
1784
|
li.dataset.body = bodyHtml;
|
|
1726
1785
|
} else {
|
|
1727
1786
|
if (li.dataset.avatar !== avatarHtml) {
|
|
1728
1787
|
li.querySelector(".avatar").outerHTML = avatarHtml;
|
|
1729
1788
|
li.dataset.avatar = avatarHtml;
|
|
1730
1789
|
}
|
|
1731
|
-
const dot = li.querySelector(".avatar
|
|
1732
|
-
if (
|
|
1733
|
-
else if (
|
|
1790
|
+
const dot = li.querySelector(".avatar .status");
|
|
1791
|
+
if (statusValue && dot && dot.dataset.status !== statusValue) dot.dataset.status = statusValue;
|
|
1792
|
+
else if (statusValue && !dot) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="status" data-status="${esc(statusValue)}"></span>`);
|
|
1734
1793
|
if (li.dataset.body !== bodyHtml) {
|
|
1735
1794
|
li.querySelectorAll(".p-body, .p-actions").forEach((el) => el.remove());
|
|
1736
1795
|
li.insertAdjacentHTML("beforeend", bodyHtml);
|
|
@@ -1851,12 +1910,12 @@
|
|
|
1851
1910
|
el.innerHTML = UI.html("hub-row", { text: m.text, tone, ref: details.refId || undefined, face: face ? UI.raw(face) : undefined, title: details.refId ? `${fullTime(m.ts)} · go to the reply` : fullTime(m.ts) });
|
|
1852
1911
|
return el;
|
|
1853
1912
|
}
|
|
1854
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: m.from === "human" ? "human" : "agent" };
|
|
1913
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: m.from === "human" ? "human" : "agent" };
|
|
1855
1914
|
const mine = m.from === "human";
|
|
1856
1915
|
el.className = "msg " + (mine ? "mine" : "agent");
|
|
1857
1916
|
el.innerHTML = `
|
|
1858
1917
|
<div class="bubble-col">
|
|
1859
|
-
<div class="head"><span class="head-av">${avatar(mine ? Object.assign(meAvatarData(), { color: p.color }) : p, 32, { vendor: true })}</span><span class="name" style="color:${p.color}">${esc(m.fromName)}</span><span class="edited" hidden></span>${mine ? UI.html("icon-button", { icon: "pencil", title: "Edit this message", kind: "ghost", size: "xs", act: "edit" }) : ""}${UI.html("icon-button", { icon: "quote", title: "Quote this message in your next one", kind: "ghost", size: "xs", act: "quote" })}${UI.html("icon-button", { icon: "pin", title: "Pin this message", kind: "ghost", size: "xs", act: "pin" })}${UI.html("icon-button", { icon: "copy", title: "Copy this message, formatted; the tool calls stay here", kind: "ghost", size: "xs", act: "copy" })}<span class="time" title="${esc(fullTime(m.ts))}">${time(m.ts)}</span></div>
|
|
1918
|
+
<div class="head"><span class="head-av">${avatar(mine ? Object.assign(meAvatarData(), { color: p.color }) : p, 32, { vendor: true, me: mine })}</span><span class="name" style="color:${p.color}">${esc(m.fromName)}</span><span class="edited" hidden></span>${mine ? UI.html("icon-button", { icon: "pencil", title: "Edit this message", kind: "ghost", size: "xs", act: "edit" }) : ""}${UI.html("icon-button", { icon: "quote", title: "Quote this message in your next one", kind: "ghost", size: "xs", act: "quote" })}${UI.html("icon-button", { icon: "pin", title: "Pin this message", kind: "ghost", size: "xs", act: "pin" })}${UI.html("icon-button", { icon: "copy", title: "Copy this message, formatted; the tool calls stay here", kind: "ghost", size: "xs", act: "copy" })}<span class="time" title="${esc(fullTime(m.ts))}">${time(m.ts)}</span></div>
|
|
1860
1919
|
<div class="bubble">
|
|
1861
1920
|
${m.skill ? `<div class="skill-invoke" title="skill invocation: the vibemates that have this skill got its instructions with this message">${ic("skills")} skill <b>${esc(m.skill.name)}</b></div>` : ""}
|
|
1862
1921
|
<div class="edit-box" hidden></div>
|
|
@@ -2106,8 +2165,17 @@
|
|
|
2106
2165
|
el.classList.toggle("hidden-by-search", !messageMatches(m));
|
|
2107
2166
|
const wasPinned = el.classList.contains("pinned");
|
|
2108
2167
|
el.classList.toggle("pinned", !!m.pinned);
|
|
2109
|
-
const pinBtn = el.querySelector('[data-act="pin"]');
|
|
2168
|
+
const pinBtn = el.querySelector('.head [data-act="pin"]');
|
|
2110
2169
|
if (pinBtn) pinBtn.title = m.pinned ? "Unpin this message" : "Pin this message";
|
|
2170
|
+
const bubble = el.querySelector(".bubble");
|
|
2171
|
+
const mark = bubble && bubble.querySelector(":scope > .pin-mark");
|
|
2172
|
+
if (m.pinned && bubble && !mark) {
|
|
2173
|
+
bubble.insertAdjacentHTML("beforeend", UI.html("icon-button", { icon: "pin", title: "Pinned: click to unpin", kind: "primary", size: "xs", act: "unpin", hook: "pin-mark" }));
|
|
2174
|
+
bubble.querySelector(":scope > .pin-mark").addEventListener("click", (e) => {
|
|
2175
|
+
e.stopPropagation();
|
|
2176
|
+
if (pinBtn) pinBtn.click();
|
|
2177
|
+
});
|
|
2178
|
+
} else if (!m.pinned && mark) mark.remove();
|
|
2111
2179
|
if (wasPinned !== !!m.pinned) renderTimeline();
|
|
2112
2180
|
const editedEl = el.querySelector(".edited");
|
|
2113
2181
|
if (editedEl) {
|
|
@@ -2289,12 +2357,9 @@
|
|
|
2289
2357
|
return [...visibilityMarkers(room).entries()].map(([seq, ps]) => `${seq}:${ps.map((p) => p.id).join(",")}`).sort().join("|");
|
|
2290
2358
|
}
|
|
2291
2359
|
function dividerElement(agents) {
|
|
2292
|
-
const el = document.createElement("div");
|
|
2293
|
-
el.className = "visibility-divider";
|
|
2294
2360
|
const names = agents.map((p) => p.name);
|
|
2295
2361
|
const label = names.length === 1 ? `${names[0]} has not seen anything above this line` : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]} have not seen anything above this line`;
|
|
2296
|
-
el
|
|
2297
|
-
return el;
|
|
2362
|
+
return UI.el("unseen-line", { label, title: "Vibemates know the room only from their own starting point: the join, a replay of the last N messages, or a restored session." });
|
|
2298
2363
|
}
|
|
2299
2364
|
|
|
2300
2365
|
const PAGE_SIZE = 50;
|
|
@@ -2706,12 +2771,12 @@
|
|
|
2706
2771
|
</div>
|
|
2707
2772
|
<div class="section" id="pp-timing">
|
|
2708
2773
|
${sectionTitle("bolt", "Timing")}
|
|
2709
|
-
${field("Reply delay override, seconds",
|
|
2774
|
+
${field("Reply delay override, seconds", `${UI.html("number-field", { id: "pp-delay", value: String(p.replyDelay ?? ""), min: 0, max: 120, step: 0.5, placeholder: `the room's: ${room.settings.replyDelay ?? 4} s` })}`, `Overrides the room's delay (${room.settings.replyDelay ?? 4} s, used only when two or more vibemates are in) for this vibemate only, even when it is alone. Empty: it follows the room.`, "Before each turn the vibemate waits a random 0–N seconds, so replies cross less often. Messages that arrive during the wait land in its backlog, so it can react to them or stay silent.")}
|
|
2710
2775
|
</div>
|
|
2711
2776
|
<div class="section danger">
|
|
2712
2777
|
${sectionTitle("bolt", "Respawn")}
|
|
2713
2778
|
<p class="hint">${esc(p.name)} comes back with an empty head: it forgets this conversation entirely. The room's history stays and you still see everything.${geekTip("A session's context cannot be erased, so the vibemate's process and session are closed and it starts a new one with no replay. Its stored session is dropped too, or a later reconnect would bring the old context back. Same thing as typing /respawn @Name in the composer.")}</p>
|
|
2714
|
-
<div class="row-btns start">${UI.html("button", { label: `Respawn ${p.name}`, icon: "bolt", kind: "danger", size: "sm", act: "respawn" })}<label class="lp-with">${UI.html("button", { label: "With the last", size: "sm", act: "respawn-mem" })}
|
|
2779
|
+
<div class="row-btns start">${UI.html("button", { label: `Respawn ${p.name}`, icon: "bolt", kind: "danger", size: "sm", act: "respawn" })}<label class="lp-with">${UI.html("button", { label: "With the last", size: "sm", act: "respawn-mem" })}${UI.html("number-field", { id: "pp-respawn-n", value: String(room.settings.replayAfterRestart ?? 10), min: 0, max: 500 })} messages</label></div>
|
|
2715
2780
|
<p class="hint">With memory: a new session that gets only ${p.notes ? "its own notes and " : ""}the last N messages of this room; the rest is gone.</p>
|
|
2716
2781
|
</div>
|
|
2717
2782
|
<div class="section">
|
|
@@ -2814,7 +2879,7 @@
|
|
|
2814
2879
|
els.detailsInner.innerHTML = `
|
|
2815
2880
|
${panelTitle("Your vibe", "how the vibemates know you")}
|
|
2816
2881
|
<div class="profile">
|
|
2817
|
-
${avatar(meAvatarData(), 84, {})}
|
|
2882
|
+
${avatar(meAvatarData(), 84, { kind: "card" })}
|
|
2818
2883
|
<h3>${esc(s.humanName || "")}</h3>
|
|
2819
2884
|
<div class="tagline">${esc(s.humanDescription || "no vibe line yet")}</div>
|
|
2820
2885
|
</div>
|
|
@@ -2868,7 +2933,7 @@
|
|
|
2868
2933
|
<div class="section">
|
|
2869
2934
|
${sectionTitle("user", "Turn taking")}
|
|
2870
2935
|
${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.")}
|
|
2871
|
-
${field("Reply delay, seconds",
|
|
2936
|
+
${field("Reply delay, seconds", `${UI.html("number-field", { id: "rp-delay", value: String(rs.replyDelay ?? 4), min: 0, max: 120, step: 0.5 })}`, "With two or more vibemates, each waits a random 0–N seconds before it answers, so replies cross less often. A vibemate alone answers at once. A vibemate's own delay (in its panel) always applies.")}
|
|
2872
2937
|
<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>
|
|
2873
2938
|
<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>
|
|
2874
2939
|
</div>
|
|
@@ -2890,8 +2955,8 @@
|
|
|
2890
2955
|
<div class="section">
|
|
2891
2956
|
${sectionTitle("chat", "Conversation")}
|
|
2892
2957
|
${field("Vibemates' own tools (files, shell, web)", `<select id="rp-tools"><option value="on-request"${rs.tools === "on-request" ? " selected" : ""}>Only when someone explicitly asks</option><option value="never"${rs.tools === "never" ? " selected" : ""}>Never (chat only)</option></select>`, null, "An instruction in every vibemate's brief; the vibemate's mode is the real limit.")}
|
|
2893
|
-
${field("Max sentences per reply",
|
|
2894
|
-
${field("Hop limit (vibemate-to-vibemate replies per human message)",
|
|
2958
|
+
${field("Max sentences per reply", `${UI.html("number-field", { id: "rp-maxlen", value: String(rs.maxSentences ?? ""), min: 1, max: 100, placeholder: "no limit" })}`)}
|
|
2959
|
+
${field("Hop limit (vibemate-to-vibemate replies per human message)", `${UI.html("number-field", { id: "rp-hops", value: String(rs.hopLimit), min: 0, max: 10000 })}`)}
|
|
2895
2960
|
</div>
|
|
2896
2961
|
<div class="section">
|
|
2897
2962
|
${sectionTitle("eye", "Referee")}
|
|
@@ -2899,12 +2964,12 @@
|
|
|
2899
2964
|
</div>
|
|
2900
2965
|
<div class="section">
|
|
2901
2966
|
${sectionTitle("save", "Instruction delivery")}
|
|
2902
|
-
${field("Full brief every N vibemate turns",
|
|
2903
|
-
${field("…or every N new context tokens",
|
|
2967
|
+
${field("Full brief every N vibemate turns", `${UI.html("number-field", { id: "rp-brief-turns", value: String(rs.fullBriefEveryTurns), min: 1, max: 10000 })}`)}
|
|
2968
|
+
${field("…or every N new context tokens", `${UI.html("number-field", { id: "rp-brief-tokens", value: String(rs.fullBriefEveryTokens), min: 1000, max: 10000000, step: 1000 })}`)}
|
|
2904
2969
|
<label class="switch"><span class="label">Repeat core rules in every header</span><input type="checkbox" id="rp-header-rules" ${rs.headerRules ? "checked" : ""}></label>
|
|
2905
2970
|
<label class="switch"><span class="label">Show vendor and model to other vibemates</span><input type="checkbox" id="rp-vendor" ${rs.showVendorInRoster ? "checked" : ""}></label>
|
|
2906
|
-
${field("Replay last N chat messages after a reconnect",
|
|
2907
|
-
${field("Missed messages a vibemate reads at most on its next turn",
|
|
2971
|
+
${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
|
+
${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.")}
|
|
2908
2973
|
</div>`,
|
|
2909
2974
|
"tools, hops, referee, briefs",
|
|
2910
2975
|
)}
|
|
@@ -2976,7 +3041,7 @@
|
|
|
2976
3041
|
.map((r) => {
|
|
2977
3042
|
const v = (s.vendorPresets || {})[r.id] || {};
|
|
2978
3043
|
return `<div class="vendor-card">
|
|
2979
|
-
<div class="vc-head"
|
|
3044
|
+
<div class="vc-head">${vendorLogo(r)}<span class="vc-name">${esc(r.vendor)}</span>${UI.html("badge", { label: "installed", tone: "ready", dot: true })}</div>
|
|
2980
3045
|
<div class="vc-fields">
|
|
2981
3046
|
${field("Model", `<input type="text" data-vendor="${r.id}" data-key="model" value="${esc(v.model || "")}" placeholder="${esc(r.defaultModel || "vibemate default")}">`)}
|
|
2982
3047
|
${field("Effort", `<input type="text" data-vendor="${r.id}" data-key="effort" value="${esc(v.effort || "")}" placeholder="${esc(r.defaultEffort || "vibemate default")}">`)}
|
|
@@ -2985,7 +3050,7 @@
|
|
|
2985
3050
|
</div>`;
|
|
2986
3051
|
})
|
|
2987
3052
|
.join("");
|
|
2988
|
-
const logo = (r) =>
|
|
3053
|
+
const logo = (r) => vendorLogo(r);
|
|
2989
3054
|
const machine =
|
|
2990
3055
|
installed
|
|
2991
3056
|
.map((r) => {
|
|
@@ -3000,6 +3065,31 @@
|
|
|
3000
3065
|
<div id="sp-form">
|
|
3001
3066
|
<div class="page-cols">
|
|
3002
3067
|
<div>
|
|
3068
|
+
<div class="section" id="sp-appearance">
|
|
3069
|
+
${sectionTitle("eye", "Appearance")}
|
|
3070
|
+
${(() => {
|
|
3071
|
+
const a = s.appearance || {};
|
|
3072
|
+
const lookId = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
3073
|
+
const look = TOKENS.looks[lookId];
|
|
3074
|
+
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("");
|
|
3076
|
+
const groups = [...new Set(TOKENS.adjustables.map((f) => f.group))];
|
|
3077
|
+
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
|
+
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.")}
|
|
3080
|
+
<div class="look-adjust" id="sp-look-adjust">
|
|
3081
|
+
<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
|
+
<p class="hint">Kept for this look alone. The sample follows as you pick; the window follows on save.</p>
|
|
3083
|
+
${groups.map((g) => `<div class="adjust-group"><h5>${esc(g)}</h5>${rows(g)}</div>`).join("")}
|
|
3084
|
+
</div>`;
|
|
3085
|
+
})()}
|
|
3086
|
+
<div class="adjust-group text"><h5>Text</h5>
|
|
3087
|
+
<div class="field row"><span class="label">Size, px<span class="hint">Only the text changes size; the boxes, buttons and panels stay. 14.5 is the default.</span></span>${UI.html("number-field", { id: "sp-chat-fs", value: String((s.appearance || {}).chatFontSize || 14.5), min: 12, max: 24, step: 0.5 })}</div>
|
|
3088
|
+
<div class="field row"><span class="label">Font${geekTip("The named fonts come with viberoom and look the same on every OS (all free, with Cyrillic); the system entries use what this machine has. A look with a font of its own (Terminal, Clay) keeps it while this stays at the default.")}</span><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></div>
|
|
3089
|
+
<div class="field row"><span class="label">Code font<span class="hint">For code blocks, paths and tool output.</span></span><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></div>
|
|
3090
|
+
<div class="bubble" id="sp-chat-sample" style="display:inline-block;font-size:${(s.appearance || {}).chatFontSize || 14.5}px">Messages will read like this, with <code>code</code> a step smaller.</div>
|
|
3091
|
+
</div>
|
|
3092
|
+
</div>
|
|
3003
3093
|
<div class="section">
|
|
3004
3094
|
${sectionTitle("lock", "Permissions")}
|
|
3005
3095
|
<label class="switch"><span class="label">Vibemates act without asking<span class="hint">Off: they ask you before editing files or running commands.</span>${geekTip('New vibemates start in their vendor\'s "act without asking" mode (Claude bypassPermissions, Codex agent-full-access, Gemini yolo, Cursor agent, OpenCode build, Copilot agent + allow_all). Change it per vibemate when summoning one, or later in its panel.')}</span><input type="checkbox" id="sp-bypass" ${s.bypassPermissionsByDefault !== false ? "checked" : ""}></label>
|
|
@@ -3007,20 +3097,13 @@
|
|
|
3007
3097
|
<div class="section">
|
|
3008
3098
|
${sectionTitle("bolt", "Pace")}
|
|
3009
3099
|
${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>`)}
|
|
3010
|
-
${field("Reply delay in new rooms, seconds",
|
|
3011
|
-
</div>
|
|
3012
|
-
<div class="section" id="sp-appearance">
|
|
3013
|
-
${sectionTitle("eye", "Appearance")}
|
|
3014
|
-
${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. Only the text changes size with it: the boxes, buttons and panels stay as they are.")}
|
|
3015
|
-
${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.")}
|
|
3016
|
-
${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.")}
|
|
3017
|
-
<div class="bubble" id="sp-chat-sample" style="display:inline-block;font-size:${(s.appearance || {}).chatFontSize || 14.5}px">Messages will read like this, with <code>code</code> a step smaller.</div>
|
|
3100
|
+
${field("Reply delay in new rooms, seconds", `${UI.html("number-field", { id: "sp-delay", value: String(d.replyDelay ?? 4), min: 0, max: 120, step: 0.5 })}`, "Used when two or more vibemates share a room; each room can change it; a vibemate can override it in its own panel.", "Before each turn a vibemate waits a random 0–N seconds, so replies cross less often. Messages that arrive meanwhile land in its backlog. A vibemate alone answers at once unless it has its own delay.")}
|
|
3018
3101
|
</div>
|
|
3019
3102
|
<div class="section" id="sp-editor">
|
|
3020
3103
|
${sectionTitle("pencil", "Open files at a line")}
|
|
3021
3104
|
<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>
|
|
3022
3105
|
<div class="chips editor-modes">
|
|
3023
|
-
${UI.html("choice", { label: "Auto", on: !["custom", "default-app"].includes((s.editor || {}).mode), data: { mode: "auto" }
|
|
3106
|
+
${UI.html("choice", { label: "Auto", on: !["custom", "default-app"].includes((s.editor || {}).mode), data: { mode: "auto" } })}
|
|
3024
3107
|
${UI.html("choice", { label: "The default app", on: (s.editor || {}).mode === "default-app", data: { mode: "default-app" } })}
|
|
3025
3108
|
${UI.html("choice", { label: "My own command", on: (s.editor || {}).mode === "custom", data: { mode: "custom" } })}
|
|
3026
3109
|
</div>
|
|
@@ -3065,9 +3148,9 @@
|
|
|
3065
3148
|
<div class="section">
|
|
3066
3149
|
${sectionTitle("rooms", "Defaults for new rooms")}
|
|
3067
3150
|
<p class="field-note">Every new room starts with these; each room can change them in its own settings.</p>
|
|
3068
|
-
${field("Hop limit",
|
|
3069
|
-
${field("Full brief every N turns",
|
|
3070
|
-
${field("Full brief every N tokens",
|
|
3151
|
+
${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
|
+
${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
|
+
${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.")}
|
|
3071
3154
|
<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>
|
|
3072
3155
|
${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.")}
|
|
3073
3156
|
</div>
|
|
@@ -3106,12 +3189,6 @@
|
|
|
3106
3189
|
showEditorCmd();
|
|
3107
3190
|
}),
|
|
3108
3191
|
);
|
|
3109
|
-
get("/api/editor")
|
|
3110
|
-
.then((r) => {
|
|
3111
|
-
const auto = $("#sp-editor-auto");
|
|
3112
|
-
if (auto) auto.textContent = r.editor ? `(${r.editor.label})` : "(none found)";
|
|
3113
|
-
})
|
|
3114
|
-
.catch(() => undefined);
|
|
3115
3192
|
const diagramSection = $("#sp-diagrams");
|
|
3116
3193
|
const previewTheme = () => ({ preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null });
|
|
3117
3194
|
const redrawPreview = () => {
|
|
@@ -3141,6 +3218,88 @@
|
|
|
3141
3218
|
if (px >= 12 && px <= 24) sample.style.fontSize = `${px}px`;
|
|
3142
3219
|
});
|
|
3143
3220
|
$("#sp-font").addEventListener("change", () => (sample.style.fontFamily = FONTS.text[$("#sp-font").value].stack));
|
|
3221
|
+
const looksBox = $("#sp-appearance");
|
|
3222
|
+
const pickedLook = () => TOKENS.looks[$("#sp-look").value] || TOKENS.current;
|
|
3223
|
+
const rowOf = (key) => looksBox.querySelector(`[data-ui="adjust-row"][data-key="${key}"]`);
|
|
3224
|
+
const inputOf = (key) => {
|
|
3225
|
+
const row = rowOf(key);
|
|
3226
|
+
return row ? row.querySelector("input") : null;
|
|
3227
|
+
};
|
|
3228
|
+
const pendingAdjust = () => {
|
|
3229
|
+
const look = pickedLook();
|
|
3230
|
+
const out = {};
|
|
3231
|
+
for (const f of TOKENS.adjustables) {
|
|
3232
|
+
const input = inputOf(f.key);
|
|
3233
|
+
const value = String(input ? input.value : "").toLowerCase();
|
|
3234
|
+
if (value && value !== String(f.of(look)).toLowerCase()) out[f.key] = value;
|
|
3235
|
+
}
|
|
3236
|
+
return out;
|
|
3237
|
+
};
|
|
3238
|
+
const paintRows = () => {
|
|
3239
|
+
const look = pickedLook();
|
|
3240
|
+
for (const f of TOKENS.adjustables) {
|
|
3241
|
+
const row = rowOf(f.key);
|
|
3242
|
+
const input = inputOf(f.key);
|
|
3243
|
+
if (!row || !input) continue;
|
|
3244
|
+
const own = String(f.of(look));
|
|
3245
|
+
UI.setState(row, input.value.toLowerCase() !== own.toLowerCase() ? "changed" : null);
|
|
3246
|
+
row.querySelector(".value").textContent = input.value;
|
|
3247
|
+
row.querySelector(".back").title = `Back to the look's own, ${own}`;
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
const paintLookPreview = () => {
|
|
3251
|
+
const preview = $("#sp-look-preview");
|
|
3252
|
+
if (!preview) return;
|
|
3253
|
+
const look = pickedLook();
|
|
3254
|
+
preview.dataset.look = look.id;
|
|
3255
|
+
for (const [name, value] of Object.entries(TOKENS.cssVars(look))) preview.style.setProperty(name, value);
|
|
3256
|
+
applyCustomVars(preview, pendingAdjust());
|
|
3257
|
+
paintRows();
|
|
3258
|
+
};
|
|
3259
|
+
const fillRows = (lookId) => {
|
|
3260
|
+
const look = TOKENS.looks[lookId] || TOKENS.current;
|
|
3261
|
+
const custom = ((state.settings.appearance || {}).custom || {})[look.id] || {};
|
|
3262
|
+
for (const f of TOKENS.adjustables) {
|
|
3263
|
+
const input = inputOf(f.key);
|
|
3264
|
+
if (input) input.value = custom[f.key] || String(f.of(look));
|
|
3265
|
+
}
|
|
3266
|
+
$("#sp-adj-name").textContent = look.label;
|
|
3267
|
+
};
|
|
3268
|
+
looksBox.querySelectorAll('.look-cards [data-ui="look-card"]').forEach((b) =>
|
|
3269
|
+
b.addEventListener("click", () => {
|
|
3270
|
+
looksBox.querySelectorAll('.look-cards [data-ui="look-card"]').forEach((x) => {
|
|
3271
|
+
UI.setState(x, x === b ? "on" : null);
|
|
3272
|
+
x.setAttribute("aria-pressed", x === b ? "true" : "false");
|
|
3273
|
+
});
|
|
3274
|
+
const input = $("#sp-look");
|
|
3275
|
+
input.value = b.dataset.look;
|
|
3276
|
+
fillRows(b.dataset.look);
|
|
3277
|
+
paintLookPreview();
|
|
3278
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3279
|
+
}),
|
|
3280
|
+
);
|
|
3281
|
+
looksBox.querySelectorAll('[data-ui="adjust-row"] input').forEach((i) => i.addEventListener("input", paintLookPreview));
|
|
3282
|
+
looksBox.addEventListener("click", (e) => {
|
|
3283
|
+
const back = e.target.closest && e.target.closest('[data-ui="adjust-row"] .back');
|
|
3284
|
+
if (!back) return;
|
|
3285
|
+
const row = back.closest('[data-ui="adjust-row"]');
|
|
3286
|
+
const f = TOKENS.adjustables.find((x) => x.key === row.dataset.key);
|
|
3287
|
+
const input = row.querySelector("input");
|
|
3288
|
+
if (!f || !input) return;
|
|
3289
|
+
input.value = String(f.of(pickedLook()));
|
|
3290
|
+
paintLookPreview();
|
|
3291
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3292
|
+
});
|
|
3293
|
+
looksBox.querySelector(".sp-adj-reset").addEventListener("click", () => {
|
|
3294
|
+
const look = pickedLook();
|
|
3295
|
+
for (const f of TOKENS.adjustables) {
|
|
3296
|
+
const input = inputOf(f.key);
|
|
3297
|
+
if (input) input.value = String(f.of(look));
|
|
3298
|
+
}
|
|
3299
|
+
paintLookPreview();
|
|
3300
|
+
$("#sp-look").dispatchEvent(new Event("change", { bubbles: true }));
|
|
3301
|
+
});
|
|
3302
|
+
paintLookPreview();
|
|
3144
3303
|
$("#sp-mono").addEventListener("change", () => sample.querySelectorAll("code").forEach((c) => (c.style.fontFamily = FONTS.mono[$("#sp-mono").value].stack)));
|
|
3145
3304
|
$("#sp-slow-copy").addEventListener("click", async () => {
|
|
3146
3305
|
const b = $("#sp-slow-copy");
|
|
@@ -3193,7 +3352,7 @@
|
|
|
3193
3352
|
checkForUpdates: $("#sp-updates").checked,
|
|
3194
3353
|
diagrams: { preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null },
|
|
3195
3354
|
editor: { mode: $("#sp-editor-mode").value, command: $("#sp-editor-cmd").value },
|
|
3196
|
-
appearance: { chatFontSize: Number($("#sp-chat-fs").value), font: $("#sp-font").value, mono: $("#sp-mono").value },
|
|
3355
|
+
appearance: { chatFontSize: Number($("#sp-chat-fs").value), font: $("#sp-font").value, mono: $("#sp-mono").value, look: $("#sp-look").value, custom: { [$("#sp-look").value]: Object.keys(pendingAdjust()).length ? pendingAdjust() : null } },
|
|
3197
3356
|
roomDefaults: {
|
|
3198
3357
|
turnTaking: $("#sp-turns").value,
|
|
3199
3358
|
replyDelay: Number($("#sp-delay").value),
|
|
@@ -3580,7 +3739,7 @@
|
|
|
3580
3739
|
els.invAgents.innerHTML = state.recipes
|
|
3581
3740
|
.map(
|
|
3582
3741
|
(r) =>
|
|
3583
|
-
`<button type="button" class="agent-tile${r.unavailableReason ? " off" : ""}" data-agent="${esc(r.id)}" title="${esc(r.unavailableReason ? `Not installed on this machine. ${r.installHint || ""}` : r.loginState === "missing" ? `Found at ${r.installedAt || "bundled"}, but not logged in: run ${r.loginCommand}` : `Found at ${r.installedAt || "bundled"}`)}"
|
|
3742
|
+
`<button type="button" class="agent-tile${r.unavailableReason ? " off" : ""}" data-agent="${esc(r.id)}" title="${esc(r.unavailableReason ? `Not installed on this machine. ${r.installHint || ""}` : r.loginState === "missing" ? `Found at ${r.installedAt || "bundled"}, but not logged in: run ${r.loginCommand}` : `Found at ${r.installedAt || "bundled"}`)}">${vendorLogo(r, "lg")}<span class="at-name">${esc(r.vendor)}</span><span class="at-sub">${r.unavailableReason ? "not installed" : r.loginState === "missing" ? "no login" : "installed"}</span></button>`,
|
|
3584
3743
|
)
|
|
3585
3744
|
.join("");
|
|
3586
3745
|
els.invAgents.querySelectorAll(".agent-tile:not(.off)").forEach((b) =>
|
|
@@ -3735,6 +3894,7 @@
|
|
|
3735
3894
|
if (!room) return closeDialog(els.rcDialog);
|
|
3736
3895
|
const globalMode = els.rcForm.querySelector('input[name="rc-mode"]:checked').value;
|
|
3737
3896
|
const replay = Number(els.rcReplay.value);
|
|
3897
|
+
if (Number.isFinite(replay) && replay !== (room.settings.replayAfterRestart ?? 10)) post(roomApi("/settings"), { replayAfterRestart: replay }).catch((error) => toast(error.message, "error"));
|
|
3738
3898
|
const rows = [...els.rcTable.querySelectorAll("tr")].map((tr) => ({ id: tr.dataset.id, choice: tr.querySelector(".rc-per").value || globalMode }));
|
|
3739
3899
|
els.rcSubmit.disabled = true;
|
|
3740
3900
|
UI.setState(els.rcSubmit, "loading");
|
|
@@ -3787,7 +3947,7 @@
|
|
|
3787
3947
|
tplEls.list.innerHTML = tpl.items
|
|
3788
3948
|
.map((t) => {
|
|
3789
3949
|
const on = tpl.current && t.id === tpl.current.id;
|
|
3790
|
-
const faces = t.vibemates.slice(0, 4).map((v) => avatar({ name: v.name, avatar: v.avatar, color: FALLBACK_COLOR }, 20, {})).join("");
|
|
3950
|
+
const faces = t.vibemates.slice(0, 4).map((v) => avatar({ name: v.name, avatar: v.avatar, color: FALLBACK_COLOR() }, 20, { ring: true })).join("");
|
|
3791
3951
|
return `<button type="button" class="tpl-item${on ? " on" : ""}" data-id="${esc(t.id)}" role="radio" aria-checked="${on ? "true" : "false"}">
|
|
3792
3952
|
<span class="tpl-emoji">${esc(t.emoji || "🧩")}</span>
|
|
3793
3953
|
<span class="tpl-body"><b>${esc(t.name)}${t.builtin ? "" : UI.html("badge", { label: "your template", size: "xs" })}${t.recommended ? UI.html("badge", { label: "recommended", tone: "attention", size: "xs" }) : ""}</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>
|
|
@@ -3895,22 +4055,22 @@
|
|
|
3895
4055
|
${stpField("Language", `<input type="text" id="stp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
3896
4056
|
${stpField("Vibemates' own tools", stpSelect("stp-tools", st.tools || "on-request", [["on-request", "Only when someone explicitly asks"], ["never", "Never (chat only)"]]))}
|
|
3897
4057
|
${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"]]))}
|
|
3898
|
-
${stpField("Reply delay, seconds",
|
|
4058
|
+
${stpField("Reply delay, seconds", `${UI.html("number-field", { id: "stp-delay", value: String(st.replyDelay ?? 4), min: 0, max: 120, step: 0.5 })}`)}
|
|
3899
4059
|
${stpSwitch("Vibemates wake each other", "stp-wake", st.agentsWakeEachOther !== false)}
|
|
3900
4060
|
${stpSwitch("Wait while you are typing", "stp-wait", st.waitWhileHumanTypes !== false)}
|
|
3901
|
-
${stpField("Hop limit",
|
|
3902
|
-
${stpField("Max sentences per reply",
|
|
4061
|
+
${stpField("Hop limit", `${UI.html("number-field", { id: "stp-hops", value: String(st.hopLimit ?? 100), min: 0, max: 10000 })}`)}
|
|
4062
|
+
${stpField("Max sentences per reply", `${UI.html("number-field", { id: "stp-maxlen", value: String(st.maxSentences ?? ""), min: 1, max: 100, placeholder: "no limit" })}`)}
|
|
3903
4063
|
${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"]]))}
|
|
3904
4064
|
${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"]]))}
|
|
3905
|
-
${stpField("Full brief every N turns",
|
|
3906
|
-
${stpField("…or every N tokens",
|
|
3907
|
-
${stpField("Replay after a reconnect",
|
|
3908
|
-
${stpField("Missed messages read at most",
|
|
4065
|
+
${stpField("Full brief every N turns", `${UI.html("number-field", { id: "stp-brief-turns", value: String(st.fullBriefEveryTurns ?? 8), min: 1, max: 10000 })}`)}
|
|
4066
|
+
${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
|
+
${stpField("Replay after a reconnect", `${UI.html("number-field", { id: "stp-replay", value: String(st.replayAfterRestart ?? 10), min: 0, max: 200 })}`)}
|
|
4068
|
+
${stpField("Missed messages read at most", `${UI.html("number-field", { id: "stp-backlog", value: String(st.backlogCap ?? 50), min: 1, max: 1000 })}`)}
|
|
3909
4069
|
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
3910
4070
|
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
3911
4071
|
</div>`;
|
|
3912
4072
|
const agentOptions = [["", "none yet: cast when the room opens"], ...state.recipes.map((r) => [r.id, r.vendor + (r.unavailableReason ? " (not installed here)" : "")])];
|
|
3913
|
-
const vms = (t.vibemates || []).map((v, i) => `<div class="stp-vm" data-i="${i}">${avatar({ name: v.name, avatar: v.avatar, color: FALLBACK_COLOR }, 36, {})}<div>
|
|
4073
|
+
const vms = (t.vibemates || []).map((v, i) => `<div class="stp-vm" data-i="${i}">${avatar({ name: v.name, avatar: v.avatar, color: FALLBACK_COLOR() }, 36, {})}<div>
|
|
3914
4074
|
<div class="stp-vm-top"><b>${esc(v.name)}</b>${UI.html("icon-button", { icon: "close", title: "Leave this vibemate out of the template", kind: "ghost", size: "sm", data: { stpRemove: true } })}</div>
|
|
3915
4075
|
<div class="stp-vm-fields">
|
|
3916
4076
|
${stpField("Vibename", `<input type="text" data-k="name" maxlength="40" value="${esc(v.name)}" required>`)}
|
|
@@ -3921,7 +4081,7 @@
|
|
|
3921
4081
|
${stpField("Model", `<input type="text" data-k="model" value="${esc(v.model || "")}" placeholder="the agent's default">`)}
|
|
3922
4082
|
${stpField("Effort", `<input type="text" data-k="effort" value="${esc(v.effort || "")}" placeholder="default">`)}
|
|
3923
4083
|
${stpField("Mode", `<input type="text" data-k="mode" value="${esc(v.mode || "")}" placeholder="default">`)}
|
|
3924
|
-
${stpField("Reply delay override, s",
|
|
4084
|
+
${stpField("Reply delay override, s", `${UI.html("number-field", { value: String(v.replyDelay ?? ""), min: 0, max: 120, step: 0.5, placeholder: "the room's", data: { k: "replyDelay" } })}`)}
|
|
3925
4085
|
${stpField("Skills, comma-separated", `<input type="text" data-k="skills" value="${esc((v.skills || []).join(", "))}">`, true)}
|
|
3926
4086
|
</div>
|
|
3927
4087
|
</div></div>`).join("");
|
|
@@ -4030,12 +4190,35 @@
|
|
|
4030
4190
|
closeDialog(els.pfDialog);
|
|
4031
4191
|
if (state.view === "home") renderHome();
|
|
4032
4192
|
toast(`Welcome, ${els.pfName.value.trim()}. Open a room and summon a vibemate.`, "info");
|
|
4193
|
+
if (state.freshVibe) offerLook();
|
|
4033
4194
|
} catch (error) {
|
|
4034
4195
|
els.pfError.textContent = error.message;
|
|
4035
4196
|
els.pfError.hidden = false;
|
|
4036
4197
|
}
|
|
4037
4198
|
});
|
|
4038
4199
|
els.pfDialog.addEventListener("cancel", (event) => event.preventDefault());
|
|
4200
|
+
function offerLook() {
|
|
4201
|
+
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("");
|
|
4203
|
+
openDialog($("#look-dialog"));
|
|
4204
|
+
}
|
|
4205
|
+
$("#look-dialog-cards").addEventListener("click", async (e) => {
|
|
4206
|
+
const card = e.target.closest('[data-ui="look-card"]');
|
|
4207
|
+
if (!card) return;
|
|
4208
|
+
$("#look-dialog-cards").querySelectorAll('[data-ui="look-card"]').forEach((x) => {
|
|
4209
|
+
UI.setState(x, x === card ? "on" : null);
|
|
4210
|
+
x.setAttribute("aria-pressed", x === card ? "true" : "false");
|
|
4211
|
+
});
|
|
4212
|
+
try {
|
|
4213
|
+
await post("/api/settings", { appearance: { look: card.dataset.look } });
|
|
4214
|
+
} catch (error) {
|
|
4215
|
+
toast(error.message, "error");
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
$("#look-form").addEventListener("submit", (e) => {
|
|
4219
|
+
e.preventDefault();
|
|
4220
|
+
closeDialog($("#look-dialog"));
|
|
4221
|
+
});
|
|
4039
4222
|
|
|
4040
4223
|
function openEraseDialog() {
|
|
4041
4224
|
els.eraseWord.value = "";
|
|
@@ -4081,12 +4264,37 @@
|
|
|
4081
4264
|
}
|
|
4082
4265
|
|
|
4083
4266
|
|
|
4267
|
+
const CUSTOM_VARS = [...new Set(TOKENS.adjustables.flatMap((f) => Object.keys(f.vars(f.kind === "scale" ? "1" : TOKENS.current.palette.black))))];
|
|
4268
|
+
function customVars(v) {
|
|
4269
|
+
const out = {};
|
|
4270
|
+
for (const f of TOKENS.adjustables) if (v[f.key]) Object.assign(out, f.vars(v[f.key]));
|
|
4271
|
+
return out;
|
|
4272
|
+
}
|
|
4273
|
+
function applyCustomVars(el, values) {
|
|
4274
|
+
for (const name of CUSTOM_VARS) el.style.removeProperty(name);
|
|
4275
|
+
for (const [name, value] of Object.entries(customVars(values || {}))) el.style.setProperty(name, value);
|
|
4276
|
+
}
|
|
4277
|
+
let appliedScheme = null;
|
|
4084
4278
|
function applyAppearance() {
|
|
4085
4279
|
const a = (state.settings || {}).appearance || {};
|
|
4086
4280
|
const root = document.documentElement;
|
|
4087
4281
|
root.style.setProperty("--fs-scale", String((a.chatFontSize || 14.5) / 14.5));
|
|
4088
|
-
|
|
4282
|
+
const look = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
4283
|
+
if (look === TOKENS.current.id) delete root.dataset.look;
|
|
4284
|
+
else root.dataset.look = look;
|
|
4285
|
+
const scheme = TOKENS.looks[look].scheme;
|
|
4286
|
+
if (appliedScheme && appliedScheme !== scheme) {
|
|
4287
|
+
document.querySelectorAll(".mermaid-block[data-rendered]").forEach((b) => delete b.dataset.rendered);
|
|
4288
|
+
renderDiagrams(document);
|
|
4289
|
+
}
|
|
4290
|
+
appliedScheme = scheme;
|
|
4291
|
+
if (look !== TOKENS.current.id && (!a.font || a.font === "nunito")) root.style.removeProperty("--font");
|
|
4292
|
+
else root.style.setProperty("--font", (FONTS.text[a.font] || FONTS.text.nunito).stack);
|
|
4089
4293
|
root.style.setProperty("--mono", (FONTS.mono[a.mono] || FONTS.mono["jetbrains-mono"]).stack);
|
|
4294
|
+
const frame = document.querySelector('meta[name="theme-color"]');
|
|
4295
|
+
if (frame) frame.content = TOKENS.looks[look].elements.browser.themeColor;
|
|
4296
|
+
applyCustomVars(root, (a.custom || {})[look] || {});
|
|
4297
|
+
document.querySelectorAll("#participants .avatar .life").forEach((ring) => setLifePath(ring, ring.parentElement));
|
|
4090
4298
|
}
|
|
4091
4299
|
|
|
4092
4300
|
let hubIdentity = null;
|
|
@@ -4700,7 +4908,7 @@
|
|
|
4700
4908
|
const waiting = room ? room.participants.filter((p) => p.kind === "agent" && p.status === "unstaffed") : [];
|
|
4701
4909
|
castBanner.hidden = waiting.length === 0;
|
|
4702
4910
|
castBanner.innerHTML = waiting.length
|
|
4703
|
-
? `<div class="cast-lead"><strong>Summon ${waiting.map((p) => esc(p.name)).join(" and ")} to begin.</strong> ${waiting.length === 1 ? "It comes" : "They come"} from the template with the character set; pick the coding agent that runs ${waiting.length === 1 ? "it" : "each"}.</div><div class="cast-list">${waiting.map((p) => `<button type="button" class="cast-card" data-id="${esc(p.id)}">${avatar(p, 44, {})}<b>${esc(p.name)}</b>${p.tagline ? `<span>"${esc(p.tagline)}"</span>` : ""}<em>Summon ${esc(p.name)}</em></button>`).join("")}</div>`
|
|
4911
|
+
? `<div class="cast-lead"><strong>Summon ${waiting.map((p) => esc(p.name)).join(" and ")} to begin.</strong> ${waiting.length === 1 ? "It comes" : "They come"} from the template with the character set; pick the coding agent that runs ${waiting.length === 1 ? "it" : "each"}.</div><div class="cast-list">${waiting.map((p) => `<button type="button" class="cast-card" data-id="${esc(p.id)}">${avatar(p, 44, { dim: "unstaffed" })}<b>${esc(p.name)}</b>${p.tagline ? `<span>"${esc(p.tagline)}"</span>` : ""}<em>Summon ${esc(p.name)}</em></button>`).join("")}</div>`
|
|
4704
4912
|
: "";
|
|
4705
4913
|
els.input.disabled = waiting.length > 0;
|
|
4706
4914
|
els.input.placeholder = waiting.length ? `Summon ${waiting.map((p) => p.name).join(" and ")} to start the conversation` : "Message the room… @Name or /skill";
|
|
@@ -5216,7 +5424,9 @@
|
|
|
5216
5424
|
if (!nodes.length) return;
|
|
5217
5425
|
const total = els.messages.scrollHeight || 1;
|
|
5218
5426
|
const h = Math.max(0, t.ticks.clientHeight - TICK_H);
|
|
5219
|
-
const
|
|
5427
|
+
const fits = els.messages.scrollHeight <= els.messages.clientHeight + 1;
|
|
5428
|
+
const stripTop = fits ? t.ticks.getBoundingClientRect().top : 0;
|
|
5429
|
+
const tops = fits ? nodes.map((el) => el.getBoundingClientRect().top - stripTop) : nodes.map(topInList);
|
|
5220
5430
|
const frag = document.createDocumentFragment();
|
|
5221
5431
|
nodes.forEach((el, i) => {
|
|
5222
5432
|
const tick = document.createElement("div");
|
|
@@ -5224,11 +5434,18 @@
|
|
|
5224
5434
|
tick.className = `tl-tick${pinned ? " pinned i i-pin" : ""}`;
|
|
5225
5435
|
tick.dataset.i = i;
|
|
5226
5436
|
if (opts.colorOf) tick.style.setProperty("--tick", opts.colorOf(room, el));
|
|
5227
|
-
tick.style.top = `${Math.round((tops[i] / total) * h)}px`;
|
|
5437
|
+
tick.style.top = `${Math.round(fits ? Math.max(0, Math.min(h, tops[i] + 6)) : (tops[i] / total) * h)}px`;
|
|
5228
5438
|
frag.appendChild(tick);
|
|
5229
5439
|
});
|
|
5230
5440
|
t.ticks.replaceChildren(frag);
|
|
5231
5441
|
updateView();
|
|
5442
|
+
if (fits && !t.following) {
|
|
5443
|
+
t.following = true;
|
|
5444
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
5445
|
+
render();
|
|
5446
|
+
t.following = false;
|
|
5447
|
+
}));
|
|
5448
|
+
}
|
|
5232
5449
|
}
|
|
5233
5450
|
function updateView() {
|
|
5234
5451
|
if (t.el.hidden) return;
|
|
@@ -5346,7 +5563,7 @@
|
|
|
5346
5563
|
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
5347
5564
|
requestAnimationFrame(() => {
|
|
5348
5565
|
if (bubbleInView(m.id)) return;
|
|
5349
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: "agent" };
|
|
5566
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: "agent" };
|
|
5350
5567
|
pushNote({ id: m.id, kind: "done", p, text: `${p.name} finished`, sub: `started ${time(m.ts)}${m.durationMs ? ` · ${spanText(m.durationMs)}` : ""}` });
|
|
5351
5568
|
});
|
|
5352
5569
|
}
|
|
@@ -5354,7 +5571,7 @@
|
|
|
5354
5571
|
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
5355
5572
|
requestAnimationFrame(() => {
|
|
5356
5573
|
if (bubbleInView(m.id)) return;
|
|
5357
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: "agent" };
|
|
5574
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: "agent" };
|
|
5358
5575
|
pushNote({ id: m.id, kind: "new", p, text: `${p.name} wrote below`, sub: time(m.ts) });
|
|
5359
5576
|
});
|
|
5360
5577
|
}
|
|
@@ -5391,7 +5608,7 @@
|
|
|
5391
5608
|
const timelines = [
|
|
5392
5609
|
createTimeline($("#timeline"), () => [...els.messages.querySelectorAll(".msg.mine:not(.hidden-by-search)")], {}),
|
|
5393
5610
|
createTimeline($("#timeline-left"), () => [...els.messages.querySelectorAll(".msg.agent:not(.hidden-by-search)")], {
|
|
5394
|
-
colorOf: (room, el) => (authorOf(room, el) || {}).color || FALLBACK_COLOR,
|
|
5611
|
+
colorOf: (room, el) => (authorOf(room, el) || {}).color || FALLBACK_COLOR(),
|
|
5395
5612
|
avatarOf: (room, el) => { const p = authorOf(room, el); return p ? avatar(p, 16, {}) : ""; },
|
|
5396
5613
|
}),
|
|
5397
5614
|
];
|
|
@@ -5404,6 +5621,7 @@
|
|
|
5404
5621
|
function updateTimelineView() { for (const t of timelines) t.updateView(); }
|
|
5405
5622
|
attachScrollHints(els.pageInner);
|
|
5406
5623
|
attachScrollHints(els.detailsInner);
|
|
5624
|
+
document.querySelectorAll("dialog.dialog").forEach((d) => attachScrollHints(d));
|
|
5407
5625
|
const composerFollowers = [$("#timeline"), $("#timeline-left"), els.mentionMenu, els.emojiMenu, els.jumpLatest];
|
|
5408
5626
|
new ResizeObserver(() => {
|
|
5409
5627
|
const h = `${els.composer.offsetHeight}px`;
|
|
@@ -5411,6 +5629,15 @@
|
|
|
5411
5629
|
renderTimeline();
|
|
5412
5630
|
}).observe(els.composer);
|
|
5413
5631
|
new ResizeObserver(() => renderTimeline()).observe(els.messages);
|
|
5632
|
+
document.fonts.ready.then(() => renderTimeline());
|
|
5633
|
+
let timelineTimer = 0;
|
|
5634
|
+
new MutationObserver(() => {
|
|
5635
|
+
if (timelineTimer) return;
|
|
5636
|
+
timelineTimer = setTimeout(() => {
|
|
5637
|
+
timelineTimer = 0;
|
|
5638
|
+
renderTimeline();
|
|
5639
|
+
}, 300);
|
|
5640
|
+
}).observe(els.messages, { childList: true, subtree: true });
|
|
5414
5641
|
|
|
5415
5642
|
const workingNow = $("#working-now");
|
|
5416
5643
|
function renderWorkingNow() {
|
|
@@ -5453,8 +5680,27 @@
|
|
|
5453
5680
|
const left = Math.max(0, Math.min(100, 100 - Math.round((100 * (p.contextUsed || 0)) / p.contextSize)));
|
|
5454
5681
|
return { left, tone: left <= 20 ? "hot" : left <= 50 ? "warm" : "ok" };
|
|
5455
5682
|
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5683
|
+
function lifePathFor(r) {
|
|
5684
|
+
const s = 2.5, e = 49.5, mid = 26;
|
|
5685
|
+
r = Math.max(0, Math.min(23.5, r));
|
|
5686
|
+
const n = (v) => String(Math.round(v * 10) / 10);
|
|
5687
|
+
if (r < 0.5) return `M${mid} ${s}H${e}V${e}H${s}V${s}H${mid}`;
|
|
5688
|
+
const a = (x, y) => `A${n(r)} ${n(r)} 0 0 1 ${n(x)} ${n(y)}`;
|
|
5689
|
+
return `M${mid} ${s}H${n(e - r)}${a(e, s + r)}V${n(e - r)}${a(e - r, e)}H${n(s + r)}${a(s, e - r)}V${n(s + r)}${a(s + r, s)}H${mid}`;
|
|
5690
|
+
}
|
|
5691
|
+
function lifeRadius(av) {
|
|
5692
|
+
const cs = getComputedStyle(av.isConnected ? av : document.documentElement);
|
|
5693
|
+
const scale = parseFloat(cs.getPropertyValue("--r-scale"));
|
|
5694
|
+
const corner = parseFloat(cs.getPropertyValue("--face-corner"));
|
|
5695
|
+
return ((Number.isFinite(corner) ? corner : 0.32) * 44 + 4) * (Number.isFinite(scale) ? scale : 1);
|
|
5696
|
+
}
|
|
5697
|
+
function setLifePath(ring, av) {
|
|
5698
|
+
const d = lifePathFor(lifeRadius(av));
|
|
5699
|
+
if (ring.dataset.d === d) return;
|
|
5700
|
+
ring.querySelectorAll("path").forEach((path) => path.setAttribute("d", d));
|
|
5701
|
+
ring.dataset.d = d;
|
|
5702
|
+
}
|
|
5703
|
+
const LIFE_RING = `<svg class="life" viewBox="0 0 52 52" aria-hidden="true"><path class="life-track" pathLength="100"/><path class="life-arc" pathLength="100"/></svg>`;
|
|
5458
5704
|
function patchLifeRing(li, p) {
|
|
5459
5705
|
const av = li.querySelector(".avatar");
|
|
5460
5706
|
if (!av) return;
|
|
@@ -5464,6 +5710,7 @@
|
|
|
5464
5710
|
ring = av.querySelector(".life");
|
|
5465
5711
|
av.classList.add("has-life");
|
|
5466
5712
|
}
|
|
5713
|
+
setLifePath(ring, av);
|
|
5467
5714
|
const { left, tone } = lifeOf(p);
|
|
5468
5715
|
const cls = `life life-${tone}`;
|
|
5469
5716
|
if (ring.getAttribute("class") !== cls) ring.setAttribute("class", cls);
|
|
@@ -5553,7 +5800,7 @@
|
|
|
5553
5800
|
${p.notes ? `<pre class="lp-notes">${esc(p.notes)}</pre><div class="lp-editor" hidden><textarea class="lp-notes-area" rows="6" maxlength="4000">${esc(p.notes)}</textarea><div class="row-btns">${UI.html("button", { label: "Clear", kind: "ghost", size: "sm", hook: "lp-notes-clear" })}${UI.html("button", { label: "Save", kind: "primary", size: "sm", hook: "lp-notes-save" })}</div></div>` : ""}
|
|
5554
5801
|
<div class="lp-respawn">
|
|
5555
5802
|
${UI.html("button", { label: "Respawn, empty head", icon: "bolt", kind: "danger", size: "sm", hook: "lp-empty", title: "A new session that knows nothing of this conversation" })}
|
|
5556
|
-
<label class="lp-with">${UI.html("button", { label: "Respawn with the last", size: "sm", hook: "lp-mem", title: `A new session that re-reads only the last N messages${p.notes ? " and its own notes" : ""}` })}
|
|
5803
|
+
<label class="lp-with">${UI.html("button", { label: "Respawn with the last", size: "sm", hook: "lp-mem", title: `A new session that re-reads only the last N messages${p.notes ? " and its own notes" : ""}` })}${UI.html("number-field", { value: String(n), min: 0, max: 500, hook: "lp-n" })} messages</label>
|
|
5557
5804
|
</div>
|
|
5558
5805
|
</div>
|
|
5559
5806
|
<div class="lp-side">${UI.html("icon-button", { icon: "close", title: "Close", size: "sm", hook: "lp-x" })}${UI.html("icon-button", { icon: "settings", title: "Open this vibemate's panel", size: "sm", hook: "lp-more" })}</div>`;
|