viberoom 0.6.1 → 0.8.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/README.md +11 -0
- package/dist/hub.js +101 -14
- package/dist/look-lint.js +180 -0
- package/dist/looks.js +151 -0
- package/dist/mcp-skills-server.js +87 -0
- package/dist/persona.js +2 -0
- package/dist/room.js +134 -4
- package/dist/server.js +70 -0
- package/dist/skills.js +23 -2
- package/package.json +1 -1
- package/ui/app.css +142 -114
- package/ui/app.js +423 -102
- package/ui/avatars.js +7 -14
- package/ui/components.css +156 -80
- package/ui/components.js +207 -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 +16 -6
- package/ui/looks.css +2831 -0
- package/ui/manifest.json +3 -3
- package/ui/theme.css +203 -96
- package/ui/tokens.js +800 -45
- package/ui/ui.js +2 -0
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,26 +162,12 @@
|
|
|
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
|
-
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
170
|
-
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
171
|
-
},
|
|
172
|
-
mono: {
|
|
173
|
-
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
174
|
-
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
175
|
-
"source-code-pro": { label: "Source Code Pro", stack: '"Source Code Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
176
|
-
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
177
|
-
},
|
|
178
|
-
};
|
|
165
|
+
const FONTS = globalThis.VIBEROOM_TOKENS.fonts;
|
|
179
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" };
|
|
180
167
|
const STATUS_TONE = { idle: "ready", queued: "waiting", starting: "waiting", thinking: "thinking", writing: "writing", error: "error", offline: "asleep", left: "asleep", unstaffed: "attention" };
|
|
181
168
|
const TOOL_STATUSES = new Set(["pending", "in_progress", "completed", "failed"]);
|
|
182
169
|
const TOKENS = globalThis.VIBEROOM_TOKENS;
|
|
183
|
-
const FALLBACK_COLOR = TOKENS.
|
|
170
|
+
const FALLBACK_COLOR = () => TOKENS.active().elements.face.fallback;
|
|
184
171
|
const WORKING_SVG = '<svg class="working" viewBox="0 0 44 35" aria-hidden="true" title="working…">'
|
|
185
172
|
+ '<g class="body">'
|
|
186
173
|
+ '<circle cx="20" cy="6.5" r="5.6" fill="currentColor"/>'
|
|
@@ -825,9 +812,13 @@
|
|
|
825
812
|
function diagramSettings(d) {
|
|
826
813
|
return d || (state.settings && state.settings.diagrams) || {};
|
|
827
814
|
}
|
|
815
|
+
function diagramPreset(d) {
|
|
816
|
+
const look = TOKENS.active();
|
|
817
|
+
return TOKENS.diagrams.forScheme(DIAGRAM_PRESETS[d.preset] || DIAGRAM_PRESETS.pop, look.scheme, look.elements.diagram.nodeInk);
|
|
818
|
+
}
|
|
828
819
|
function mermaidThemeVariables(d) {
|
|
829
820
|
d = diagramSettings(d);
|
|
830
|
-
const preset =
|
|
821
|
+
const preset = diagramPreset(d);
|
|
831
822
|
const vars = Object.assign({}, preset, { fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font").trim() || "Nunito, sans-serif", fontSize: "13px" });
|
|
832
823
|
delete vars.label;
|
|
833
824
|
delete vars.palette;
|
|
@@ -841,19 +832,19 @@
|
|
|
841
832
|
}
|
|
842
833
|
function diagramPalette(d) {
|
|
843
834
|
d = diagramSettings(d);
|
|
844
|
-
const preset =
|
|
835
|
+
const preset = diagramPreset(d);
|
|
845
836
|
return preset.palette && !d.primary ? preset.palette : null;
|
|
846
837
|
}
|
|
847
|
-
const
|
|
838
|
+
const mermaidCss = () => [
|
|
848
839
|
".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.
|
|
840
|
+
`.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
841
|
".edgePath .path, .flowchart-link, .messageLine0, .messageLine1, .transition, .relation { stroke-width: 2px; }",
|
|
851
842
|
".edgeLabel, .edgeLabel p { font-weight: 700; }",
|
|
852
843
|
".cluster rect { stroke-dasharray: 4 3; stroke-width: 1.5px; }",
|
|
853
844
|
".cluster-label, .cluster-label p { font-weight: 800; }",
|
|
854
845
|
].join(" ");
|
|
855
846
|
function paintDiagram(root, palette) {
|
|
856
|
-
const ink = TOKENS.
|
|
847
|
+
const ink = TOKENS.active().elements.diagram.nodeInk;
|
|
857
848
|
const byKey = new Map();
|
|
858
849
|
const pick = (key) => {
|
|
859
850
|
if (!byKey.has(key)) byKey.set(key, palette[byKey.size % palette.length]);
|
|
@@ -911,7 +902,7 @@
|
|
|
911
902
|
theme: "base",
|
|
912
903
|
securityLevel: "strict",
|
|
913
904
|
themeVariables: mermaidThemeVariables(d),
|
|
914
|
-
themeCSS:
|
|
905
|
+
themeCSS: mermaidCss(),
|
|
915
906
|
flowchart: { curve: "basis", padding: 14, nodeSpacing: 44, rankSpacing: 52 },
|
|
916
907
|
});
|
|
917
908
|
for (const block of blocks) {
|
|
@@ -993,7 +984,7 @@
|
|
|
993
984
|
}
|
|
994
985
|
function meAvatarData() {
|
|
995
986
|
const s = state.settings || {};
|
|
996
|
-
return { name: s.humanName || "You", color: TOKENS.
|
|
987
|
+
return { name: s.humanName || "You", color: TOKENS.active().elements.face.humanInk, avatar: s.humanAvatar, kind: "human" };
|
|
997
988
|
}
|
|
998
989
|
function copyableHtml(el) {
|
|
999
990
|
const clone = (el.querySelector(".text > .words") || el.querySelector(".text")).cloneNode(true);
|
|
@@ -1044,11 +1035,26 @@
|
|
|
1044
1035
|
t.hidden = !t.hidden;
|
|
1045
1036
|
b.classList.toggle("on", !t.hidden);
|
|
1046
1037
|
});
|
|
1038
|
+
document.addEventListener("click", (e) => {
|
|
1039
|
+
const step = e.target.closest && e.target.closest('[data-ui="number-field"] > .steps > button');
|
|
1040
|
+
if (!step) return;
|
|
1041
|
+
e.preventDefault();
|
|
1042
|
+
const input = step.closest('[data-ui="number-field"]').querySelector("input");
|
|
1043
|
+
if (!input || input.disabled) return;
|
|
1044
|
+
try {
|
|
1045
|
+
if (step.dataset.act === "up") input.stepUp();
|
|
1046
|
+
else input.stepDown();
|
|
1047
|
+
} catch {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1051
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1052
|
+
});
|
|
1047
1053
|
function field(label, inputHtml, hint, geekText) {
|
|
1048
1054
|
return `<label class="field"><span class="label">${label}${geekText ? geekTip(geekText) : ""}</span>${inputHtml}${hint ? `<span class="hint">${hint}</span>` : ""}</label>`;
|
|
1049
1055
|
}
|
|
1050
|
-
function vendorLogo(r) {
|
|
1051
|
-
return
|
|
1056
|
+
function vendorLogo(r, size) {
|
|
1057
|
+
return UI.html("logo-tile", { icon: r.icon || "", letter: r.vendor[0], size: size || "md", title: r.vendor });
|
|
1052
1058
|
}
|
|
1053
1059
|
function slowTasksHtml() {
|
|
1054
1060
|
if (!slowTasks.length) return '<p class="hint">Nothing over 8 ms so far in this window.</p>';
|
|
@@ -1146,11 +1152,18 @@
|
|
|
1146
1152
|
return h % 360;
|
|
1147
1153
|
}
|
|
1148
1154
|
function roomMark(room, lg) {
|
|
1149
|
-
const hue = roomHue(room);
|
|
1150
1155
|
const emoji = (room.settings && room.settings.emoji) || "";
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1156
|
+
return UI.html("room-mark", { hue: roomHue(room), emoji: emoji || undefined, letter: emoji ? undefined : room.name.trim()[0] || "?", size: lg ? "lg" : "md" });
|
|
1157
|
+
}
|
|
1158
|
+
function lookSampleHtml() {
|
|
1159
|
+
const withIcon = state.recipes.filter((r) => r.icon);
|
|
1160
|
+
const lead = withIcon[0] || state.recipes[0];
|
|
1161
|
+
const sam = { name: "Sam", kind: "agent", color: TOKENS.active().elements.face.avatarDefault, avatar: "🦊", agentType: lead ? lead.id : "", agentLabel: lead ? lead.vendor : "" };
|
|
1162
|
+
const marks = (withIcon.length ? withIcon : state.recipes).slice(0, 3).map((r) => vendorLogo(r, "sm")).join("");
|
|
1163
|
+
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>
|
|
1164
|
+
<div class="msg mine"><div class="bubble-col"><div class="bubble">Your message.</div></div></div>
|
|
1165
|
+
<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>
|
|
1166
|
+
<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
1167
|
}
|
|
1155
1168
|
function roomTitle(room) {
|
|
1156
1169
|
const emoji = (room.settings && room.settings.emoji) || "";
|
|
@@ -1168,10 +1181,11 @@
|
|
|
1168
1181
|
|
|
1169
1182
|
function attachScrollHints(box) {
|
|
1170
1183
|
if (!box || box.dataset.hinted) return;
|
|
1184
|
+
const inside = box.tagName === "DIALOG";
|
|
1171
1185
|
const frame = box.parentElement;
|
|
1172
|
-
if (!frame) return;
|
|
1186
|
+
if (!frame && !inside) return;
|
|
1173
1187
|
box.dataset.hinted = "1";
|
|
1174
|
-
if (getComputedStyle(frame).position === "static") frame.style.position = "relative";
|
|
1188
|
+
if (!inside && getComputedStyle(frame).position === "static") frame.style.position = "relative";
|
|
1175
1189
|
const make = (dir) => {
|
|
1176
1190
|
const b = document.createElement("button");
|
|
1177
1191
|
b.type = "button";
|
|
@@ -1180,7 +1194,13 @@
|
|
|
1180
1194
|
b.innerHTML = ic(dir === "up" ? "chevrons-up" : "chevrons-down");
|
|
1181
1195
|
b.hidden = true;
|
|
1182
1196
|
b.addEventListener("click", () => box.scrollBy({ top: (dir === "up" ? -1 : 1) * Math.max(120, box.clientHeight * 0.85), behavior: "smooth" }));
|
|
1183
|
-
|
|
1197
|
+
if (inside) {
|
|
1198
|
+
const slot = document.createElement("div");
|
|
1199
|
+
slot.className = `scroll-hint-slot ${dir}`;
|
|
1200
|
+
slot.appendChild(b);
|
|
1201
|
+
if (dir === "up") box.prepend(slot);
|
|
1202
|
+
else box.append(slot);
|
|
1203
|
+
} else frame.appendChild(b);
|
|
1184
1204
|
return b;
|
|
1185
1205
|
};
|
|
1186
1206
|
const up = make("up");
|
|
@@ -1427,7 +1447,7 @@
|
|
|
1427
1447
|
renderRailRooms();
|
|
1428
1448
|
const s = state.settings;
|
|
1429
1449
|
if (s) {
|
|
1430
|
-
els.railMeAvatar.innerHTML = avatar(meAvatarData(), 44, {});
|
|
1450
|
+
els.railMeAvatar.innerHTML = avatar(meAvatarData(), 44, { kind: "bare" });
|
|
1431
1451
|
els.railMeLabel.textContent = s.humanName;
|
|
1432
1452
|
}
|
|
1433
1453
|
const open = els.app.classList.contains("rail-open");
|
|
@@ -1510,7 +1530,7 @@
|
|
|
1510
1530
|
const q = state.roomSearch.toLowerCase();
|
|
1511
1531
|
const all = document.createElement("li");
|
|
1512
1532
|
all.className = state.skillsRoom ? "" : "selected";
|
|
1513
|
-
all.innerHTML =
|
|
1533
|
+
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
1534
|
all.addEventListener("click", () => {
|
|
1515
1535
|
state.skillsRoom = null;
|
|
1516
1536
|
renderSideSkills();
|
|
@@ -1568,6 +1588,7 @@
|
|
|
1568
1588
|
{ 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
1589
|
{ 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
1590
|
{ 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." },
|
|
1591
|
+
{ 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
1592
|
];
|
|
1572
1593
|
const STEPS = [
|
|
1573
1594
|
{ 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 +1600,7 @@
|
|
|
1579
1600
|
const name = s.humanName || "there";
|
|
1580
1601
|
const rooms = sortedRooms().slice(0, 4);
|
|
1581
1602
|
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("");
|
|
1603
|
+
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
1604
|
els.homeView.innerHTML = `
|
|
1584
1605
|
<div class="home-inner">
|
|
1585
1606
|
<section class="hero">
|
|
@@ -1589,6 +1610,7 @@
|
|
|
1589
1610
|
<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
1611
|
<div class="hero-actions">
|
|
1591
1612
|
${UI.html("button", { label: "Open room", icon: "rooms", size: "cta", id: "home-open-room", hook: "hero-cta" })}
|
|
1613
|
+
${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
1614
|
</div>
|
|
1593
1615
|
</div>
|
|
1594
1616
|
<div class="hero-art" aria-hidden="true">
|
|
@@ -1608,7 +1630,7 @@
|
|
|
1608
1630
|
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
1631
|
})
|
|
1610
1632
|
.join("")}
|
|
1611
|
-
${rooms.length ? "" : `<div class="home-room empty-room"
|
|
1633
|
+
${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
1634
|
</div>
|
|
1613
1635
|
</section>
|
|
1614
1636
|
<section class="home-section">
|
|
@@ -1640,6 +1662,15 @@
|
|
|
1640
1662
|
setView("rooms");
|
|
1641
1663
|
remember("view", "rooms");
|
|
1642
1664
|
});
|
|
1665
|
+
const changeLook = $("#home-change-look");
|
|
1666
|
+
if (changeLook) changeLook.addEventListener("click", () => {
|
|
1667
|
+
setView("settings");
|
|
1668
|
+
remember("view", "settings");
|
|
1669
|
+
requestAnimationFrame(() => {
|
|
1670
|
+
const section = $("#sp-appearance");
|
|
1671
|
+
if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1643
1674
|
const all = $("#home-all-rooms");
|
|
1644
1675
|
if (all) all.addEventListener("click", () => setView("rooms"));
|
|
1645
1676
|
els.homeView.querySelectorAll(".home-room[data-room]").forEach((b) => b.addEventListener("click", () => selectRoom(b.dataset.room)));
|
|
@@ -1666,7 +1697,7 @@
|
|
|
1666
1697
|
const st = roomStats(room);
|
|
1667
1698
|
const card = document.createElement("div");
|
|
1668
1699
|
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("");
|
|
1700
|
+
const faces = st.agents.slice(0, 5).map((p) => avatar(p, 26, { vendor: true, ring: true })).join("");
|
|
1670
1701
|
card.innerHTML = `
|
|
1671
1702
|
<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
1703
|
<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 +1737,8 @@
|
|
|
1706
1737
|
: asleep
|
|
1707
1738
|
? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
|
|
1708
1739
|
: 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
|
|
1740
|
+
const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true, muted: p.muted, me: p.kind === "human", dim: unstaffed ? "unstaffed" : asleep ? "asleep" : undefined });
|
|
1741
|
+
const statusValue = p.kind === "agent" ? shown || "idle" : "";
|
|
1711
1742
|
const bodyHtml = `<div class="p-body">
|
|
1712
1743
|
<div class="p-name"><span>${esc(p.name)}</span>${p.muted ? UI.html("badge", { label: "muted", tone: "muted" }) : ""}${status}</div>
|
|
1713
1744
|
<div class="p-sub">${esc(sub)}</div>
|
|
@@ -1721,16 +1752,16 @@
|
|
|
1721
1752
|
li.dataset.id = p.id;
|
|
1722
1753
|
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
1754
|
li.dataset.avatar = avatarHtml;
|
|
1724
|
-
if (
|
|
1755
|
+
if (statusValue) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="status" data-status="${esc(statusValue)}"></span>`);
|
|
1725
1756
|
li.dataset.body = bodyHtml;
|
|
1726
1757
|
} else {
|
|
1727
1758
|
if (li.dataset.avatar !== avatarHtml) {
|
|
1728
1759
|
li.querySelector(".avatar").outerHTML = avatarHtml;
|
|
1729
1760
|
li.dataset.avatar = avatarHtml;
|
|
1730
1761
|
}
|
|
1731
|
-
const dot = li.querySelector(".avatar
|
|
1732
|
-
if (
|
|
1733
|
-
else if (
|
|
1762
|
+
const dot = li.querySelector(".avatar .status");
|
|
1763
|
+
if (statusValue && dot && dot.dataset.status !== statusValue) dot.dataset.status = statusValue;
|
|
1764
|
+
else if (statusValue && !dot) li.querySelector(".avatar").insertAdjacentHTML("beforeend", `<span class="status" data-status="${esc(statusValue)}"></span>`);
|
|
1734
1765
|
if (li.dataset.body !== bodyHtml) {
|
|
1735
1766
|
li.querySelectorAll(".p-body, .p-actions").forEach((el) => el.remove());
|
|
1736
1767
|
li.insertAdjacentHTML("beforeend", bodyHtml);
|
|
@@ -1851,12 +1882,12 @@
|
|
|
1851
1882
|
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
1883
|
return el;
|
|
1853
1884
|
}
|
|
1854
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: m.from === "human" ? "human" : "agent" };
|
|
1885
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: m.from === "human" ? "human" : "agent" };
|
|
1855
1886
|
const mine = m.from === "human";
|
|
1856
1887
|
el.className = "msg " + (mine ? "mine" : "agent");
|
|
1857
1888
|
el.innerHTML = `
|
|
1858
1889
|
<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>
|
|
1890
|
+
<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
1891
|
<div class="bubble">
|
|
1861
1892
|
${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
1893
|
<div class="edit-box" hidden></div>
|
|
@@ -2090,6 +2121,7 @@
|
|
|
2090
2121
|
const card = choice.closest('[data-ui="ask-card"]');
|
|
2091
2122
|
if (choice.dataset.act === "permit") post(roomApi(`/permissions/${encodeURIComponent(card.dataset.key)}`), { optionId: choice.dataset.option || null }).catch(showError);
|
|
2092
2123
|
else if (choice.dataset.act === "decide") post(roomApi(`/proposals/${encodeURIComponent(card.dataset.key)}`), { accept: choice.dataset.answer === "apply" }).catch(showError);
|
|
2124
|
+
else if (choice.dataset.act === "try-look") tryLook(choice.dataset.look);
|
|
2093
2125
|
return;
|
|
2094
2126
|
}
|
|
2095
2127
|
const chip = e.target.closest('[data-ui="tool-call"] > [data-ui="chip"]');
|
|
@@ -2106,8 +2138,17 @@
|
|
|
2106
2138
|
el.classList.toggle("hidden-by-search", !messageMatches(m));
|
|
2107
2139
|
const wasPinned = el.classList.contains("pinned");
|
|
2108
2140
|
el.classList.toggle("pinned", !!m.pinned);
|
|
2109
|
-
const pinBtn = el.querySelector('[data-act="pin"]');
|
|
2141
|
+
const pinBtn = el.querySelector('.head [data-act="pin"]');
|
|
2110
2142
|
if (pinBtn) pinBtn.title = m.pinned ? "Unpin this message" : "Pin this message";
|
|
2143
|
+
const bubble = el.querySelector(".bubble");
|
|
2144
|
+
const mark = bubble && bubble.querySelector(":scope > .pin-mark");
|
|
2145
|
+
if (m.pinned && bubble && !mark) {
|
|
2146
|
+
bubble.insertAdjacentHTML("beforeend", UI.html("icon-button", { icon: "pin", title: "Pinned: click to unpin", kind: "primary", size: "xs", act: "unpin", hook: "pin-mark" }));
|
|
2147
|
+
bubble.querySelector(":scope > .pin-mark").addEventListener("click", (e) => {
|
|
2148
|
+
e.stopPropagation();
|
|
2149
|
+
if (pinBtn) pinBtn.click();
|
|
2150
|
+
});
|
|
2151
|
+
} else if (!m.pinned && mark) mark.remove();
|
|
2111
2152
|
if (wasPinned !== !!m.pinned) renderTimeline();
|
|
2112
2153
|
const editedEl = el.querySelector(".edited");
|
|
2113
2154
|
if (editedEl) {
|
|
@@ -2289,12 +2330,9 @@
|
|
|
2289
2330
|
return [...visibilityMarkers(room).entries()].map(([seq, ps]) => `${seq}:${ps.map((p) => p.id).join(",")}`).sort().join("|");
|
|
2290
2331
|
}
|
|
2291
2332
|
function dividerElement(agents) {
|
|
2292
|
-
const el = document.createElement("div");
|
|
2293
|
-
el.className = "visibility-divider";
|
|
2294
2333
|
const names = agents.map((p) => p.name);
|
|
2295
2334
|
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;
|
|
2335
|
+
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
2336
|
}
|
|
2299
2337
|
|
|
2300
2338
|
const PAGE_SIZE = 50;
|
|
@@ -2565,6 +2603,7 @@
|
|
|
2565
2603
|
rows.push(`<div class="prop-row"><b>Room rules</b>${lines.join("")}</div>`);
|
|
2566
2604
|
} 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>`);
|
|
2567
2605
|
}
|
|
2606
|
+
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>`);
|
|
2568
2607
|
for (const v of p.vibemates || []) {
|
|
2569
2608
|
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>`);
|
|
2570
2609
|
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>`);
|
|
@@ -2573,9 +2612,15 @@
|
|
|
2573
2612
|
}
|
|
2574
2613
|
function renderProposal(room, p) {
|
|
2575
2614
|
const existing = els.messages.querySelector(`[data-ui="ask-card"][data-kind="proposal"][data-key="${CSS.escape(p.key)}"]`);
|
|
2615
|
+
const lookRow = (p.appearance || []).find((c) => c.key === "look");
|
|
2616
|
+
const lookTarget = lookRow && TOKENS.looks[lookRow.to];
|
|
2617
|
+
const windowNote = p.appearance && p.appearance.length
|
|
2618
|
+
? `<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>`
|
|
2619
|
+
: "";
|
|
2576
2620
|
const body =
|
|
2577
2621
|
(p.why ? `<div class="prop-why">${esc(p.why)}</div>` : "") +
|
|
2578
2622
|
`<div class="prop-diff">${proposalDiffHtml(p)}</div>` +
|
|
2623
|
+
windowNote +
|
|
2579
2624
|
(p.warnings && p.warnings.length ? `<ul class="prop-warn">${p.warnings.map((w) => `<li>${esc(w)}</li>`).join("")}</ul>` : "") +
|
|
2580
2625
|
(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>` : "");
|
|
2581
2626
|
const outcome = p.status === "pending" ? undefined : p.status === "applied" ? `applied${p.skipped && p.skipped.length ? ` · not applied: ${p.skipped.join("; ")}` : ""}` : "rejected";
|
|
@@ -2706,12 +2751,12 @@
|
|
|
2706
2751
|
</div>
|
|
2707
2752
|
<div class="section" id="pp-timing">
|
|
2708
2753
|
${sectionTitle("bolt", "Timing")}
|
|
2709
|
-
${field("Reply delay override, seconds",
|
|
2754
|
+
${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
2755
|
</div>
|
|
2711
2756
|
<div class="section danger">
|
|
2712
2757
|
${sectionTitle("bolt", "Respawn")}
|
|
2713
2758
|
<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" })}
|
|
2759
|
+
<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
2760
|
<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
2761
|
</div>
|
|
2717
2762
|
<div class="section">
|
|
@@ -2814,7 +2859,7 @@
|
|
|
2814
2859
|
els.detailsInner.innerHTML = `
|
|
2815
2860
|
${panelTitle("Your vibe", "how the vibemates know you")}
|
|
2816
2861
|
<div class="profile">
|
|
2817
|
-
${avatar(meAvatarData(), 84, {})}
|
|
2862
|
+
${avatar(meAvatarData(), 84, { kind: "card" })}
|
|
2818
2863
|
<h3>${esc(s.humanName || "")}</h3>
|
|
2819
2864
|
<div class="tagline">${esc(s.humanDescription || "no vibe line yet")}</div>
|
|
2820
2865
|
</div>
|
|
@@ -2868,7 +2913,7 @@
|
|
|
2868
2913
|
<div class="section">
|
|
2869
2914
|
${sectionTitle("user", "Turn taking")}
|
|
2870
2915
|
${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",
|
|
2916
|
+
${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
2917
|
<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
2918
|
<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
2919
|
</div>
|
|
@@ -2890,8 +2935,8 @@
|
|
|
2890
2935
|
<div class="section">
|
|
2891
2936
|
${sectionTitle("chat", "Conversation")}
|
|
2892
2937
|
${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)",
|
|
2938
|
+
${field("Max sentences per reply", `${UI.html("number-field", { id: "rp-maxlen", value: String(rs.maxSentences ?? ""), min: 1, max: 100, placeholder: "no limit" })}`)}
|
|
2939
|
+
${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
2940
|
</div>
|
|
2896
2941
|
<div class="section">
|
|
2897
2942
|
${sectionTitle("eye", "Referee")}
|
|
@@ -2899,12 +2944,12 @@
|
|
|
2899
2944
|
</div>
|
|
2900
2945
|
<div class="section">
|
|
2901
2946
|
${sectionTitle("save", "Instruction delivery")}
|
|
2902
|
-
${field("Full brief every N vibemate turns",
|
|
2903
|
-
${field("…or every N new context tokens",
|
|
2947
|
+
${field("Full brief every N vibemate turns", `${UI.html("number-field", { id: "rp-brief-turns", value: String(rs.fullBriefEveryTurns), min: 1, max: 10000 })}`)}
|
|
2948
|
+
${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
2949
|
<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
2950
|
<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",
|
|
2951
|
+
${field("Replay last N chat messages after a reconnect", `${UI.html("number-field", { id: "rp-replay", value: String(rs.replayAfterRestart), min: 0, max: 200 })}`)}
|
|
2952
|
+
${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
2953
|
</div>`,
|
|
2909
2954
|
"tools, hops, referee, briefs",
|
|
2910
2955
|
)}
|
|
@@ -2976,7 +3021,7 @@
|
|
|
2976
3021
|
.map((r) => {
|
|
2977
3022
|
const v = (s.vendorPresets || {})[r.id] || {};
|
|
2978
3023
|
return `<div class="vendor-card">
|
|
2979
|
-
<div class="vc-head"
|
|
3024
|
+
<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
3025
|
<div class="vc-fields">
|
|
2981
3026
|
${field("Model", `<input type="text" data-vendor="${r.id}" data-key="model" value="${esc(v.model || "")}" placeholder="${esc(r.defaultModel || "vibemate default")}">`)}
|
|
2982
3027
|
${field("Effort", `<input type="text" data-vendor="${r.id}" data-key="effort" value="${esc(v.effort || "")}" placeholder="${esc(r.defaultEffort || "vibemate default")}">`)}
|
|
@@ -2985,7 +3030,7 @@
|
|
|
2985
3030
|
</div>`;
|
|
2986
3031
|
})
|
|
2987
3032
|
.join("");
|
|
2988
|
-
const logo = (r) =>
|
|
3033
|
+
const logo = (r) => vendorLogo(r);
|
|
2989
3034
|
const machine =
|
|
2990
3035
|
installed
|
|
2991
3036
|
.map((r) => {
|
|
@@ -3000,6 +3045,31 @@
|
|
|
3000
3045
|
<div id="sp-form">
|
|
3001
3046
|
<div class="page-cols">
|
|
3002
3047
|
<div>
|
|
3048
|
+
<div class="section" id="sp-appearance">
|
|
3049
|
+
${sectionTitle("eye", "Appearance")}
|
|
3050
|
+
${(() => {
|
|
3051
|
+
const a = s.appearance || {};
|
|
3052
|
+
const lookId = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
3053
|
+
const look = TOKENS.looks[lookId];
|
|
3054
|
+
const custom = (a.custom || {})[lookId] || {};
|
|
3055
|
+
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("");
|
|
3056
|
+
const groups = [...new Set(TOKENS.adjustables.map((f) => f.group))];
|
|
3057
|
+
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("");
|
|
3058
|
+
return `<div class="look-preview" id="sp-look-preview">${lookSampleHtml()}</div>
|
|
3059
|
+
${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.")}
|
|
3060
|
+
<div class="look-adjust" id="sp-look-adjust">
|
|
3061
|
+
<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>
|
|
3062
|
+
<p class="hint">Kept for this look alone. The sample follows as you pick; the window follows on save.</p>
|
|
3063
|
+
${groups.map((g) => `<div class="adjust-group"><h5>${esc(g)}</h5>${rows(g)}</div>`).join("")}
|
|
3064
|
+
</div>`;
|
|
3065
|
+
})()}
|
|
3066
|
+
<div class="adjust-group text"><h5>Text</h5>
|
|
3067
|
+
<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>
|
|
3068
|
+
<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>
|
|
3069
|
+
<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>
|
|
3070
|
+
<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>
|
|
3071
|
+
</div>
|
|
3072
|
+
</div>
|
|
3003
3073
|
<div class="section">
|
|
3004
3074
|
${sectionTitle("lock", "Permissions")}
|
|
3005
3075
|
<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 +3077,13 @@
|
|
|
3007
3077
|
<div class="section">
|
|
3008
3078
|
${sectionTitle("bolt", "Pace")}
|
|
3009
3079
|
${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>
|
|
3080
|
+
${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
3081
|
</div>
|
|
3019
3082
|
<div class="section" id="sp-editor">
|
|
3020
3083
|
${sectionTitle("pencil", "Open files at a line")}
|
|
3021
3084
|
<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
3085
|
<div class="chips editor-modes">
|
|
3023
|
-
${UI.html("choice", { label: "Auto", on: !["custom", "default-app"].includes((s.editor || {}).mode), data: { mode: "auto" }
|
|
3086
|
+
${UI.html("choice", { label: "Auto", on: !["custom", "default-app"].includes((s.editor || {}).mode), data: { mode: "auto" } })}
|
|
3024
3087
|
${UI.html("choice", { label: "The default app", on: (s.editor || {}).mode === "default-app", data: { mode: "default-app" } })}
|
|
3025
3088
|
${UI.html("choice", { label: "My own command", on: (s.editor || {}).mode === "custom", data: { mode: "custom" } })}
|
|
3026
3089
|
</div>
|
|
@@ -3065,9 +3128,9 @@
|
|
|
3065
3128
|
<div class="section">
|
|
3066
3129
|
${sectionTitle("rooms", "Defaults for new rooms")}
|
|
3067
3130
|
<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",
|
|
3131
|
+
${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.")}
|
|
3132
|
+
${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.")}
|
|
3133
|
+
${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
3134
|
<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
3135
|
${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
3136
|
</div>
|
|
@@ -3106,12 +3169,6 @@
|
|
|
3106
3169
|
showEditorCmd();
|
|
3107
3170
|
}),
|
|
3108
3171
|
);
|
|
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
3172
|
const diagramSection = $("#sp-diagrams");
|
|
3116
3173
|
const previewTheme = () => ({ preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null });
|
|
3117
3174
|
const redrawPreview = () => {
|
|
@@ -3141,6 +3198,135 @@
|
|
|
3141
3198
|
if (px >= 12 && px <= 24) sample.style.fontSize = `${px}px`;
|
|
3142
3199
|
});
|
|
3143
3200
|
$("#sp-font").addEventListener("change", () => (sample.style.fontFamily = FONTS.text[$("#sp-font").value].stack));
|
|
3201
|
+
const looksBox = $("#sp-appearance");
|
|
3202
|
+
const pickedLook = () => TOKENS.looks[$("#sp-look").value] || TOKENS.current;
|
|
3203
|
+
const rowOf = (key) => looksBox.querySelector(`[data-ui="adjust-row"][data-key="${key}"]`);
|
|
3204
|
+
const inputOf = (key) => {
|
|
3205
|
+
const row = rowOf(key);
|
|
3206
|
+
return row ? row.querySelector("input") : null;
|
|
3207
|
+
};
|
|
3208
|
+
const pendingAdjust = () => {
|
|
3209
|
+
const look = pickedLook();
|
|
3210
|
+
const out = {};
|
|
3211
|
+
for (const f of TOKENS.adjustables) {
|
|
3212
|
+
const input = inputOf(f.key);
|
|
3213
|
+
const value = String(input ? input.value : "").toLowerCase();
|
|
3214
|
+
if (value && value !== String(f.of(look)).toLowerCase()) out[f.key] = value;
|
|
3215
|
+
}
|
|
3216
|
+
return out;
|
|
3217
|
+
};
|
|
3218
|
+
const paintRows = () => {
|
|
3219
|
+
const look = pickedLook();
|
|
3220
|
+
for (const f of TOKENS.adjustables) {
|
|
3221
|
+
const row = rowOf(f.key);
|
|
3222
|
+
const input = inputOf(f.key);
|
|
3223
|
+
if (!row || !input) continue;
|
|
3224
|
+
const own = String(f.of(look));
|
|
3225
|
+
UI.setState(row, input.value.toLowerCase() !== own.toLowerCase() ? "changed" : null);
|
|
3226
|
+
row.querySelector(".value").textContent = input.value;
|
|
3227
|
+
row.querySelector(".back").title = `Back to the look's own, ${own}`;
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
const paintLookPreview = () => {
|
|
3231
|
+
const preview = $("#sp-look-preview");
|
|
3232
|
+
if (!preview) return;
|
|
3233
|
+
const look = pickedLook();
|
|
3234
|
+
preview.dataset.look = look.id;
|
|
3235
|
+
for (const [name, value] of Object.entries(TOKENS.cssVars(look))) preview.style.setProperty(name, value);
|
|
3236
|
+
applyCustomVars(preview, pendingAdjust());
|
|
3237
|
+
paintRows();
|
|
3238
|
+
};
|
|
3239
|
+
const fillRows = (lookId) => {
|
|
3240
|
+
const look = TOKENS.looks[lookId] || TOKENS.current;
|
|
3241
|
+
const custom = ((state.settings.appearance || {}).custom || {})[look.id] || {};
|
|
3242
|
+
for (const f of TOKENS.adjustables) {
|
|
3243
|
+
const input = inputOf(f.key);
|
|
3244
|
+
if (input) input.value = custom[f.key] || String(f.of(look));
|
|
3245
|
+
}
|
|
3246
|
+
$("#sp-adj-name").textContent = look.label;
|
|
3247
|
+
};
|
|
3248
|
+
looksBox.querySelectorAll('.look-cards [data-ui="look-card"]').forEach((b) =>
|
|
3249
|
+
b.addEventListener("click", () => {
|
|
3250
|
+
looksBox.querySelectorAll('.look-cards [data-ui="look-card"]').forEach((x) => {
|
|
3251
|
+
UI.setState(x, x === b ? "on" : null);
|
|
3252
|
+
x.setAttribute("aria-pressed", x === b ? "true" : "false");
|
|
3253
|
+
});
|
|
3254
|
+
const input = $("#sp-look");
|
|
3255
|
+
input.value = b.dataset.look;
|
|
3256
|
+
fillRows(b.dataset.look);
|
|
3257
|
+
paintLookPreview();
|
|
3258
|
+
paintOwnRow();
|
|
3259
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3260
|
+
}),
|
|
3261
|
+
);
|
|
3262
|
+
const paintOwnRow = () => {
|
|
3263
|
+
const own = looksBox.querySelector("#sp-look-own .own-only");
|
|
3264
|
+
if (own) own.hidden = !pickedLook().custom;
|
|
3265
|
+
};
|
|
3266
|
+
looksBox.querySelector(".sp-look-import").addEventListener("click", () => $("#sp-look-file").click());
|
|
3267
|
+
$("#sp-look-file").addEventListener("change", async () => {
|
|
3268
|
+
const file = $("#sp-look-file").files && $("#sp-look-file").files[0];
|
|
3269
|
+
$("#sp-look-file").value = "";
|
|
3270
|
+
if (!file) return;
|
|
3271
|
+
try {
|
|
3272
|
+
const spec = JSON.parse(await file.text());
|
|
3273
|
+
const checked = await post("/api/looks/check", { spec });
|
|
3274
|
+
if (!checked.ok) throw new Error(`${file.name} is not a look viberoom can wear: ${(checked.errors || []).map((x) => x.message).join("; ")}`);
|
|
3275
|
+
const taken = TOKENS.looks[checked.id];
|
|
3276
|
+
if (taken && !taken.custom) throw new Error(`"${checked.id}" is the id of a look viberoom ships; change the id in the file`);
|
|
3277
|
+
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;
|
|
3278
|
+
const saved = await post("/api/looks", { spec, replace: !!taken });
|
|
3279
|
+
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");
|
|
3280
|
+
} catch (error) {
|
|
3281
|
+
showError(error);
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
looksBox.querySelector(".sp-look-export").addEventListener("click", () => {
|
|
3285
|
+
const look = pickedLook();
|
|
3286
|
+
const spec = state.looks.find((l) => l.id === look.id);
|
|
3287
|
+
if (!spec) return;
|
|
3288
|
+
const a = document.createElement("a");
|
|
3289
|
+
a.href = URL.createObjectURL(new Blob([`${JSON.stringify(spec, null, 2)}\n`], { type: "application/json" }));
|
|
3290
|
+
a.download = `${look.id}.json`;
|
|
3291
|
+
document.body.appendChild(a);
|
|
3292
|
+
a.click();
|
|
3293
|
+
a.remove();
|
|
3294
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
|
3295
|
+
});
|
|
3296
|
+
looksBox.querySelector(".sp-look-delete").addEventListener("click", async () => {
|
|
3297
|
+
const look = pickedLook();
|
|
3298
|
+
if (!look.custom) return;
|
|
3299
|
+
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 });
|
|
3300
|
+
if (!ok) return;
|
|
3301
|
+
try {
|
|
3302
|
+
await post("/api/looks/remove", { id: look.id });
|
|
3303
|
+
toast(`Look "${look.label}" deleted.`, "success");
|
|
3304
|
+
} catch (error) {
|
|
3305
|
+
showError(error);
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
looksBox.querySelectorAll('[data-ui="adjust-row"] input').forEach((i) => i.addEventListener("input", paintLookPreview));
|
|
3309
|
+
looksBox.addEventListener("click", (e) => {
|
|
3310
|
+
const back = e.target.closest && e.target.closest('[data-ui="adjust-row"] .back');
|
|
3311
|
+
if (!back) return;
|
|
3312
|
+
const row = back.closest('[data-ui="adjust-row"]');
|
|
3313
|
+
const f = TOKENS.adjustables.find((x) => x.key === row.dataset.key);
|
|
3314
|
+
const input = row.querySelector("input");
|
|
3315
|
+
if (!f || !input) return;
|
|
3316
|
+
input.value = String(f.of(pickedLook()));
|
|
3317
|
+
paintLookPreview();
|
|
3318
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3319
|
+
});
|
|
3320
|
+
looksBox.querySelector(".sp-adj-reset").addEventListener("click", () => {
|
|
3321
|
+
const look = pickedLook();
|
|
3322
|
+
for (const f of TOKENS.adjustables) {
|
|
3323
|
+
const input = inputOf(f.key);
|
|
3324
|
+
if (input) input.value = String(f.of(look));
|
|
3325
|
+
}
|
|
3326
|
+
paintLookPreview();
|
|
3327
|
+
$("#sp-look").dispatchEvent(new Event("change", { bubbles: true }));
|
|
3328
|
+
});
|
|
3329
|
+
paintLookPreview();
|
|
3144
3330
|
$("#sp-mono").addEventListener("change", () => sample.querySelectorAll("code").forEach((c) => (c.style.fontFamily = FONTS.mono[$("#sp-mono").value].stack)));
|
|
3145
3331
|
$("#sp-slow-copy").addEventListener("click", async () => {
|
|
3146
3332
|
const b = $("#sp-slow-copy");
|
|
@@ -3193,7 +3379,7 @@
|
|
|
3193
3379
|
checkForUpdates: $("#sp-updates").checked,
|
|
3194
3380
|
diagrams: { preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null },
|
|
3195
3381
|
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 },
|
|
3382
|
+
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
3383
|
roomDefaults: {
|
|
3198
3384
|
turnTaking: $("#sp-turns").value,
|
|
3199
3385
|
replyDelay: Number($("#sp-delay").value),
|
|
@@ -3580,7 +3766,7 @@
|
|
|
3580
3766
|
els.invAgents.innerHTML = state.recipes
|
|
3581
3767
|
.map(
|
|
3582
3768
|
(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"}`)}"
|
|
3769
|
+
`<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
3770
|
)
|
|
3585
3771
|
.join("");
|
|
3586
3772
|
els.invAgents.querySelectorAll(".agent-tile:not(.off)").forEach((b) =>
|
|
@@ -3735,6 +3921,7 @@
|
|
|
3735
3921
|
if (!room) return closeDialog(els.rcDialog);
|
|
3736
3922
|
const globalMode = els.rcForm.querySelector('input[name="rc-mode"]:checked').value;
|
|
3737
3923
|
const replay = Number(els.rcReplay.value);
|
|
3924
|
+
if (Number.isFinite(replay) && replay !== (room.settings.replayAfterRestart ?? 10)) post(roomApi("/settings"), { replayAfterRestart: replay }).catch((error) => toast(error.message, "error"));
|
|
3738
3925
|
const rows = [...els.rcTable.querySelectorAll("tr")].map((tr) => ({ id: tr.dataset.id, choice: tr.querySelector(".rc-per").value || globalMode }));
|
|
3739
3926
|
els.rcSubmit.disabled = true;
|
|
3740
3927
|
UI.setState(els.rcSubmit, "loading");
|
|
@@ -3787,7 +3974,7 @@
|
|
|
3787
3974
|
tplEls.list.innerHTML = tpl.items
|
|
3788
3975
|
.map((t) => {
|
|
3789
3976
|
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("");
|
|
3977
|
+
const faces = t.vibemates.slice(0, 4).map((v) => avatar({ name: v.name, avatar: v.avatar, color: FALLBACK_COLOR() }, 20, { ring: true })).join("");
|
|
3791
3978
|
return `<button type="button" class="tpl-item${on ? " on" : ""}" data-id="${esc(t.id)}" role="radio" aria-checked="${on ? "true" : "false"}">
|
|
3792
3979
|
<span class="tpl-emoji">${esc(t.emoji || "🧩")}</span>
|
|
3793
3980
|
<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 +4082,22 @@
|
|
|
3895
4082
|
${stpField("Language", `<input type="text" id="stp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
3896
4083
|
${stpField("Vibemates' own tools", stpSelect("stp-tools", st.tools || "on-request", [["on-request", "Only when someone explicitly asks"], ["never", "Never (chat only)"]]))}
|
|
3897
4084
|
${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",
|
|
4085
|
+
${stpField("Reply delay, seconds", `${UI.html("number-field", { id: "stp-delay", value: String(st.replyDelay ?? 4), min: 0, max: 120, step: 0.5 })}`)}
|
|
3899
4086
|
${stpSwitch("Vibemates wake each other", "stp-wake", st.agentsWakeEachOther !== false)}
|
|
3900
4087
|
${stpSwitch("Wait while you are typing", "stp-wait", st.waitWhileHumanTypes !== false)}
|
|
3901
|
-
${stpField("Hop limit",
|
|
3902
|
-
${stpField("Max sentences per reply",
|
|
4088
|
+
${stpField("Hop limit", `${UI.html("number-field", { id: "stp-hops", value: String(st.hopLimit ?? 100), min: 0, max: 10000 })}`)}
|
|
4089
|
+
${stpField("Max sentences per reply", `${UI.html("number-field", { id: "stp-maxlen", value: String(st.maxSentences ?? ""), min: 1, max: 100, placeholder: "no limit" })}`)}
|
|
3903
4090
|
${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
4091
|
${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",
|
|
4092
|
+
${stpField("Full brief every N turns", `${UI.html("number-field", { id: "stp-brief-turns", value: String(st.fullBriefEveryTurns ?? 8), min: 1, max: 10000 })}`)}
|
|
4093
|
+
${stpField("…or every N tokens", `${UI.html("number-field", { id: "stp-brief-tokens", value: String(st.fullBriefEveryTokens ?? 20000), min: 1000, max: 10000000, step: 1000 })}`)}
|
|
4094
|
+
${stpField("Replay after a reconnect", `${UI.html("number-field", { id: "stp-replay", value: String(st.replayAfterRestart ?? 10), min: 0, max: 200 })}`)}
|
|
4095
|
+
${stpField("Missed messages read at most", `${UI.html("number-field", { id: "stp-backlog", value: String(st.backlogCap ?? 50), min: 1, max: 1000 })}`)}
|
|
3909
4096
|
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
3910
4097
|
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
3911
4098
|
</div>`;
|
|
3912
4099
|
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>
|
|
4100
|
+
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
4101
|
<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
4102
|
<div class="stp-vm-fields">
|
|
3916
4103
|
${stpField("Vibename", `<input type="text" data-k="name" maxlength="40" value="${esc(v.name)}" required>`)}
|
|
@@ -3921,7 +4108,7 @@
|
|
|
3921
4108
|
${stpField("Model", `<input type="text" data-k="model" value="${esc(v.model || "")}" placeholder="the agent's default">`)}
|
|
3922
4109
|
${stpField("Effort", `<input type="text" data-k="effort" value="${esc(v.effort || "")}" placeholder="default">`)}
|
|
3923
4110
|
${stpField("Mode", `<input type="text" data-k="mode" value="${esc(v.mode || "")}" placeholder="default">`)}
|
|
3924
|
-
${stpField("Reply delay override, s",
|
|
4111
|
+
${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
4112
|
${stpField("Skills, comma-separated", `<input type="text" data-k="skills" value="${esc((v.skills || []).join(", "))}">`, true)}
|
|
3926
4113
|
</div>
|
|
3927
4114
|
</div></div>`).join("");
|
|
@@ -4030,12 +4217,35 @@
|
|
|
4030
4217
|
closeDialog(els.pfDialog);
|
|
4031
4218
|
if (state.view === "home") renderHome();
|
|
4032
4219
|
toast(`Welcome, ${els.pfName.value.trim()}. Open a room and summon a vibemate.`, "info");
|
|
4220
|
+
if (state.freshVibe) offerLook();
|
|
4033
4221
|
} catch (error) {
|
|
4034
4222
|
els.pfError.textContent = error.message;
|
|
4035
4223
|
els.pfError.hidden = false;
|
|
4036
4224
|
}
|
|
4037
4225
|
});
|
|
4038
4226
|
els.pfDialog.addEventListener("cancel", (event) => event.preventDefault());
|
|
4227
|
+
function offerLook() {
|
|
4228
|
+
const current = (state.settings.appearance || {}).look || TOKENS.current.id;
|
|
4229
|
+
$("#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("");
|
|
4230
|
+
openDialog($("#look-dialog"));
|
|
4231
|
+
}
|
|
4232
|
+
$("#look-dialog-cards").addEventListener("click", async (e) => {
|
|
4233
|
+
const card = e.target.closest('[data-ui="look-card"]');
|
|
4234
|
+
if (!card) return;
|
|
4235
|
+
$("#look-dialog-cards").querySelectorAll('[data-ui="look-card"]').forEach((x) => {
|
|
4236
|
+
UI.setState(x, x === card ? "on" : null);
|
|
4237
|
+
x.setAttribute("aria-pressed", x === card ? "true" : "false");
|
|
4238
|
+
});
|
|
4239
|
+
try {
|
|
4240
|
+
await post("/api/settings", { appearance: { look: card.dataset.look } });
|
|
4241
|
+
} catch (error) {
|
|
4242
|
+
toast(error.message, "error");
|
|
4243
|
+
}
|
|
4244
|
+
});
|
|
4245
|
+
$("#look-form").addEventListener("submit", (e) => {
|
|
4246
|
+
e.preventDefault();
|
|
4247
|
+
closeDialog($("#look-dialog"));
|
|
4248
|
+
});
|
|
4039
4249
|
|
|
4040
4250
|
function openEraseDialog() {
|
|
4041
4251
|
els.eraseWord.value = "";
|
|
@@ -4081,12 +4291,75 @@
|
|
|
4081
4291
|
}
|
|
4082
4292
|
|
|
4083
4293
|
|
|
4294
|
+
const CUSTOM_VARS = [...new Set(TOKENS.adjustables.flatMap((f) => Object.keys(f.vars(f.kind === "scale" ? "1" : TOKENS.current.palette.black))))];
|
|
4295
|
+
function customVars(v) {
|
|
4296
|
+
const out = {};
|
|
4297
|
+
for (const f of TOKENS.adjustables) if (v[f.key]) Object.assign(out, f.vars(v[f.key]));
|
|
4298
|
+
return out;
|
|
4299
|
+
}
|
|
4300
|
+
function applyCustomVars(el, values) {
|
|
4301
|
+
for (const name of CUSTOM_VARS) el.style.removeProperty(name);
|
|
4302
|
+
for (const [name, value] of Object.entries(customVars(values || {}))) el.style.setProperty(name, value);
|
|
4303
|
+
}
|
|
4304
|
+
function registerLooks(specs) {
|
|
4305
|
+
for (const id of Object.keys(TOKENS.looks)) if (TOKENS.looks[id].custom) delete TOKENS.looks[id];
|
|
4306
|
+
for (const spec of specs || []) {
|
|
4307
|
+
try {
|
|
4308
|
+
TOKENS.looks[spec.id] = TOKENS.make(spec);
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
console.warn(`look ${spec.id} could not be built: ${error.message}`);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function refreshLooksCss() {
|
|
4315
|
+
const link = $("#looks-custom");
|
|
4316
|
+
if (link) link.href = `/looks-custom.css?v=${Date.now()}`;
|
|
4317
|
+
}
|
|
4318
|
+
function lookTag(look) {
|
|
4319
|
+
return look.custom ? `${(state.settings && state.settings.humanName) || "Your"}'s look` : undefined;
|
|
4320
|
+
}
|
|
4321
|
+
function tryLook(id) {
|
|
4322
|
+
const root = document.documentElement;
|
|
4323
|
+
if (root.dataset.tried === id || !TOKENS.looks[id]) {
|
|
4324
|
+
applyAppearance();
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
if (id === TOKENS.current.id) delete root.dataset.look;
|
|
4328
|
+
else root.dataset.look = id;
|
|
4329
|
+
applyCustomVars(root, (((state.settings || {}).appearance || {}).custom || {})[id] || {});
|
|
4330
|
+
root.dataset.tried = id;
|
|
4331
|
+
refreshTryButtons();
|
|
4332
|
+
}
|
|
4333
|
+
function refreshTryButtons() {
|
|
4334
|
+
const tried = document.documentElement.dataset.tried || "";
|
|
4335
|
+
document.querySelectorAll('[data-ui="ask-card"] [data-act="try-look"]').forEach((b) => {
|
|
4336
|
+
const label = b.querySelector(".label") || b;
|
|
4337
|
+
label.textContent = tried === b.dataset.look ? "Back" : "Try it on";
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
let appliedScheme = null;
|
|
4084
4341
|
function applyAppearance() {
|
|
4085
4342
|
const a = (state.settings || {}).appearance || {};
|
|
4086
4343
|
const root = document.documentElement;
|
|
4344
|
+
delete root.dataset.tried;
|
|
4087
4345
|
root.style.setProperty("--fs-scale", String((a.chatFontSize || 14.5) / 14.5));
|
|
4088
|
-
|
|
4346
|
+
const look = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
4347
|
+
if (look === TOKENS.current.id) delete root.dataset.look;
|
|
4348
|
+
else root.dataset.look = look;
|
|
4349
|
+
const scheme = TOKENS.looks[look].scheme;
|
|
4350
|
+
if (appliedScheme && appliedScheme !== scheme) {
|
|
4351
|
+
document.querySelectorAll(".mermaid-block[data-rendered]").forEach((b) => delete b.dataset.rendered);
|
|
4352
|
+
renderDiagrams(document);
|
|
4353
|
+
}
|
|
4354
|
+
appliedScheme = scheme;
|
|
4355
|
+
if (look !== TOKENS.current.id && (!a.font || a.font === "nunito")) root.style.removeProperty("--font");
|
|
4356
|
+
else root.style.setProperty("--font", (FONTS.text[a.font] || FONTS.text.nunito).stack);
|
|
4089
4357
|
root.style.setProperty("--mono", (FONTS.mono[a.mono] || FONTS.mono["jetbrains-mono"]).stack);
|
|
4358
|
+
const frame = document.querySelector('meta[name="theme-color"]');
|
|
4359
|
+
if (frame) frame.content = TOKENS.looks[look].elements.browser.themeColor;
|
|
4360
|
+
applyCustomVars(root, (a.custom || {})[look] || {});
|
|
4361
|
+
document.querySelectorAll("#participants .avatar .life").forEach((ring) => setLifePath(ring, ring.parentElement));
|
|
4362
|
+
refreshTryButtons();
|
|
4090
4363
|
}
|
|
4091
4364
|
|
|
4092
4365
|
let hubIdentity = null;
|
|
@@ -4099,6 +4372,8 @@
|
|
|
4099
4372
|
}
|
|
4100
4373
|
if (identity) hubIdentity = identity;
|
|
4101
4374
|
state.settings = snapshot.settings;
|
|
4375
|
+
state.looks = snapshot.looks || [];
|
|
4376
|
+
registerLooks(state.looks);
|
|
4102
4377
|
applyAppearance();
|
|
4103
4378
|
state.update = snapshot.update || null;
|
|
4104
4379
|
renderUpdatePop();
|
|
@@ -4277,6 +4552,13 @@
|
|
|
4277
4552
|
if (state.view === "skills" && !editingInDetails()) renderSkillsPage();
|
|
4278
4553
|
if (state.detailsOpen && !editingInDetails()) renderDetails();
|
|
4279
4554
|
},
|
|
4555
|
+
looks: (m) => {
|
|
4556
|
+
state.looks = m.looks || [];
|
|
4557
|
+
registerLooks(state.looks);
|
|
4558
|
+
refreshLooksCss();
|
|
4559
|
+
applyAppearance();
|
|
4560
|
+
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
4561
|
+
},
|
|
4280
4562
|
settings: (m) => {
|
|
4281
4563
|
state.settings = m.settings;
|
|
4282
4564
|
applyAppearance();
|
|
@@ -4700,7 +4982,7 @@
|
|
|
4700
4982
|
const waiting = room ? room.participants.filter((p) => p.kind === "agent" && p.status === "unstaffed") : [];
|
|
4701
4983
|
castBanner.hidden = waiting.length === 0;
|
|
4702
4984
|
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>`
|
|
4985
|
+
? `<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
4986
|
: "";
|
|
4705
4987
|
els.input.disabled = waiting.length > 0;
|
|
4706
4988
|
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 +5498,9 @@
|
|
|
5216
5498
|
if (!nodes.length) return;
|
|
5217
5499
|
const total = els.messages.scrollHeight || 1;
|
|
5218
5500
|
const h = Math.max(0, t.ticks.clientHeight - TICK_H);
|
|
5219
|
-
const
|
|
5501
|
+
const fits = els.messages.scrollHeight <= els.messages.clientHeight + 1;
|
|
5502
|
+
const stripTop = fits ? t.ticks.getBoundingClientRect().top : 0;
|
|
5503
|
+
const tops = fits ? nodes.map((el) => el.getBoundingClientRect().top - stripTop) : nodes.map(topInList);
|
|
5220
5504
|
const frag = document.createDocumentFragment();
|
|
5221
5505
|
nodes.forEach((el, i) => {
|
|
5222
5506
|
const tick = document.createElement("div");
|
|
@@ -5224,11 +5508,18 @@
|
|
|
5224
5508
|
tick.className = `tl-tick${pinned ? " pinned i i-pin" : ""}`;
|
|
5225
5509
|
tick.dataset.i = i;
|
|
5226
5510
|
if (opts.colorOf) tick.style.setProperty("--tick", opts.colorOf(room, el));
|
|
5227
|
-
tick.style.top = `${Math.round((tops[i] / total) * h)}px`;
|
|
5511
|
+
tick.style.top = `${Math.round(fits ? Math.max(0, Math.min(h, tops[i] + 6)) : (tops[i] / total) * h)}px`;
|
|
5228
5512
|
frag.appendChild(tick);
|
|
5229
5513
|
});
|
|
5230
5514
|
t.ticks.replaceChildren(frag);
|
|
5231
5515
|
updateView();
|
|
5516
|
+
if (fits && !t.following) {
|
|
5517
|
+
t.following = true;
|
|
5518
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
5519
|
+
render();
|
|
5520
|
+
t.following = false;
|
|
5521
|
+
}));
|
|
5522
|
+
}
|
|
5232
5523
|
}
|
|
5233
5524
|
function updateView() {
|
|
5234
5525
|
if (t.el.hidden) return;
|
|
@@ -5346,7 +5637,7 @@
|
|
|
5346
5637
|
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
5347
5638
|
requestAnimationFrame(() => {
|
|
5348
5639
|
if (bubbleInView(m.id)) return;
|
|
5349
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: "agent" };
|
|
5640
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: "agent" };
|
|
5350
5641
|
pushNote({ id: m.id, kind: "done", p, text: `${p.name} finished`, sub: `started ${time(m.ts)}${m.durationMs ? ` · ${spanText(m.durationMs)}` : ""}` });
|
|
5351
5642
|
});
|
|
5352
5643
|
}
|
|
@@ -5354,7 +5645,7 @@
|
|
|
5354
5645
|
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
5355
5646
|
requestAnimationFrame(() => {
|
|
5356
5647
|
if (bubbleInView(m.id)) return;
|
|
5357
|
-
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: "agent" };
|
|
5648
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR(), kind: "agent" };
|
|
5358
5649
|
pushNote({ id: m.id, kind: "new", p, text: `${p.name} wrote below`, sub: time(m.ts) });
|
|
5359
5650
|
});
|
|
5360
5651
|
}
|
|
@@ -5391,7 +5682,7 @@
|
|
|
5391
5682
|
const timelines = [
|
|
5392
5683
|
createTimeline($("#timeline"), () => [...els.messages.querySelectorAll(".msg.mine:not(.hidden-by-search)")], {}),
|
|
5393
5684
|
createTimeline($("#timeline-left"), () => [...els.messages.querySelectorAll(".msg.agent:not(.hidden-by-search)")], {
|
|
5394
|
-
colorOf: (room, el) => (authorOf(room, el) || {}).color || FALLBACK_COLOR,
|
|
5685
|
+
colorOf: (room, el) => (authorOf(room, el) || {}).color || FALLBACK_COLOR(),
|
|
5395
5686
|
avatarOf: (room, el) => { const p = authorOf(room, el); return p ? avatar(p, 16, {}) : ""; },
|
|
5396
5687
|
}),
|
|
5397
5688
|
];
|
|
@@ -5404,6 +5695,7 @@
|
|
|
5404
5695
|
function updateTimelineView() { for (const t of timelines) t.updateView(); }
|
|
5405
5696
|
attachScrollHints(els.pageInner);
|
|
5406
5697
|
attachScrollHints(els.detailsInner);
|
|
5698
|
+
document.querySelectorAll("dialog.dialog").forEach((d) => attachScrollHints(d));
|
|
5407
5699
|
const composerFollowers = [$("#timeline"), $("#timeline-left"), els.mentionMenu, els.emojiMenu, els.jumpLatest];
|
|
5408
5700
|
new ResizeObserver(() => {
|
|
5409
5701
|
const h = `${els.composer.offsetHeight}px`;
|
|
@@ -5411,6 +5703,15 @@
|
|
|
5411
5703
|
renderTimeline();
|
|
5412
5704
|
}).observe(els.composer);
|
|
5413
5705
|
new ResizeObserver(() => renderTimeline()).observe(els.messages);
|
|
5706
|
+
document.fonts.ready.then(() => renderTimeline());
|
|
5707
|
+
let timelineTimer = 0;
|
|
5708
|
+
new MutationObserver(() => {
|
|
5709
|
+
if (timelineTimer) return;
|
|
5710
|
+
timelineTimer = setTimeout(() => {
|
|
5711
|
+
timelineTimer = 0;
|
|
5712
|
+
renderTimeline();
|
|
5713
|
+
}, 300);
|
|
5714
|
+
}).observe(els.messages, { childList: true, subtree: true });
|
|
5414
5715
|
|
|
5415
5716
|
const workingNow = $("#working-now");
|
|
5416
5717
|
function renderWorkingNow() {
|
|
@@ -5453,8 +5754,27 @@
|
|
|
5453
5754
|
const left = Math.max(0, Math.min(100, 100 - Math.round((100 * (p.contextUsed || 0)) / p.contextSize)));
|
|
5454
5755
|
return { left, tone: left <= 20 ? "hot" : left <= 50 ? "warm" : "ok" };
|
|
5455
5756
|
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5757
|
+
function lifePathFor(r) {
|
|
5758
|
+
const s = 2.5, e = 49.5, mid = 26;
|
|
5759
|
+
r = Math.max(0, Math.min(23.5, r));
|
|
5760
|
+
const n = (v) => String(Math.round(v * 10) / 10);
|
|
5761
|
+
if (r < 0.5) return `M${mid} ${s}H${e}V${e}H${s}V${s}H${mid}`;
|
|
5762
|
+
const a = (x, y) => `A${n(r)} ${n(r)} 0 0 1 ${n(x)} ${n(y)}`;
|
|
5763
|
+
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}`;
|
|
5764
|
+
}
|
|
5765
|
+
function lifeRadius(av) {
|
|
5766
|
+
const cs = getComputedStyle(av.isConnected ? av : document.documentElement);
|
|
5767
|
+
const scale = parseFloat(cs.getPropertyValue("--r-scale"));
|
|
5768
|
+
const corner = parseFloat(cs.getPropertyValue("--face-corner"));
|
|
5769
|
+
return ((Number.isFinite(corner) ? corner : 0.32) * 44 + 4) * (Number.isFinite(scale) ? scale : 1);
|
|
5770
|
+
}
|
|
5771
|
+
function setLifePath(ring, av) {
|
|
5772
|
+
const d = lifePathFor(lifeRadius(av));
|
|
5773
|
+
if (ring.dataset.d === d) return;
|
|
5774
|
+
ring.querySelectorAll("path").forEach((path) => path.setAttribute("d", d));
|
|
5775
|
+
ring.dataset.d = d;
|
|
5776
|
+
}
|
|
5777
|
+
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
5778
|
function patchLifeRing(li, p) {
|
|
5459
5779
|
const av = li.querySelector(".avatar");
|
|
5460
5780
|
if (!av) return;
|
|
@@ -5464,6 +5784,7 @@
|
|
|
5464
5784
|
ring = av.querySelector(".life");
|
|
5465
5785
|
av.classList.add("has-life");
|
|
5466
5786
|
}
|
|
5787
|
+
setLifePath(ring, av);
|
|
5467
5788
|
const { left, tone } = lifeOf(p);
|
|
5468
5789
|
const cls = `life life-${tone}`;
|
|
5469
5790
|
if (ring.getAttribute("class") !== cls) ring.setAttribute("class", cls);
|
|
@@ -5553,7 +5874,7 @@
|
|
|
5553
5874
|
${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
5875
|
<div class="lp-respawn">
|
|
5555
5876
|
${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" : ""}` })}
|
|
5877
|
+
<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
5878
|
</div>
|
|
5558
5879
|
</div>
|
|
5559
5880
|
<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>`;
|