viberoom 0.7.0 → 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/README.md +11 -0
- package/dist/hub.js +94 -48
- 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 +20 -16
- package/ui/app.js +107 -33
- package/ui/components.css +96 -115
- package/ui/components.js +8 -3
- package/ui/index.html +1 -0
- package/ui/looks.css +462 -77
- package/ui/theme.css +90 -13
- package/ui/tokens.js +526 -57
package/ui/app.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
recipes: [],
|
|
8
8
|
roomDefaults: null,
|
|
9
9
|
skills: [],
|
|
10
|
+
looks: [],
|
|
10
11
|
version: null,
|
|
11
12
|
rooms: new Map(),
|
|
12
13
|
currentRoomId: null,
|
|
@@ -161,36 +162,7 @@
|
|
|
161
162
|
} catch {
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const FONTS =
|
|
165
|
-
text: {
|
|
166
|
-
nunito: { label: "Nunito (default)", stack: '"Nunito", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
167
|
-
inter: { label: "Inter", stack: '"Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
168
|
-
"noto-sans": { label: "Noto Sans", stack: '"Noto Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
169
|
-
"open-sans": { label: "Open Sans", stack: '"Open Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
170
|
-
"source-sans-3": { label: "Source Sans 3", stack: '"Source Sans 3", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
171
|
-
"ibm-plex-sans": { label: "IBM Plex Sans", stack: '"IBM Plex Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
172
|
-
"manrope": { label: "Manrope", stack: '"Manrope", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
173
|
-
"rubik": { label: "Rubik", stack: '"Rubik", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
174
|
-
"montserrat": { label: "Montserrat", stack: '"Montserrat", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
175
|
-
"golos-text": { label: "Golos Text", stack: '"Golos Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
176
|
-
"exo-2": { label: "Exo 2", stack: '"Exo 2", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
177
|
-
"comfortaa": { label: "Comfortaa", stack: '"Comfortaa", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
178
|
-
"ubuntu-sans": { label: "Ubuntu Sans", stack: '"Ubuntu Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
179
|
-
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
180
|
-
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
181
|
-
},
|
|
182
|
-
mono: {
|
|
183
|
-
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
184
|
-
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
185
|
-
"source-code-pro": { label: "Source Code Pro", stack: '"Source Code Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
186
|
-
"ibm-plex-mono": { label: "IBM Plex Mono", stack: '"IBM Plex Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
187
|
-
"pt-mono": { label: "PT Mono", stack: '"PT Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
188
|
-
"victor-mono": { label: "Victor Mono", stack: '"Victor Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
189
|
-
"anonymous-pro": { label: "Anonymous Pro", stack: '"Anonymous Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
190
|
-
"cascadia-code": { label: "Cascadia Code", stack: '"Cascadia Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
191
|
-
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
192
|
-
},
|
|
193
|
-
};
|
|
165
|
+
const FONTS = globalThis.VIBEROOM_TOKENS.fonts;
|
|
194
166
|
const STATUS_LABEL = { unstaffed: "needs a coding agent", starting: "starting…", idle: "ready", queued: "waiting…", thinking: "thinking…", writing: "writing…", error: "error", offline: "offline", left: "left" };
|
|
195
167
|
const STATUS_TONE = { idle: "ready", queued: "waiting", starting: "waiting", thinking: "thinking", writing: "writing", error: "error", offline: "asleep", left: "asleep", unstaffed: "attention" };
|
|
196
168
|
const TOOL_STATUSES = new Set(["pending", "in_progress", "completed", "failed"]);
|
|
@@ -2149,6 +2121,7 @@
|
|
|
2149
2121
|
const card = choice.closest('[data-ui="ask-card"]');
|
|
2150
2122
|
if (choice.dataset.act === "permit") post(roomApi(`/permissions/${encodeURIComponent(card.dataset.key)}`), { optionId: choice.dataset.option || null }).catch(showError);
|
|
2151
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);
|
|
2152
2125
|
return;
|
|
2153
2126
|
}
|
|
2154
2127
|
const chip = e.target.closest('[data-ui="tool-call"] > [data-ui="chip"]');
|
|
@@ -2630,6 +2603,7 @@
|
|
|
2630
2603
|
rows.push(`<div class="prop-row"><b>Room rules</b>${lines.join("")}</div>`);
|
|
2631
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>`);
|
|
2632
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>`);
|
|
2633
2607
|
for (const v of p.vibemates || []) {
|
|
2634
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>`);
|
|
2635
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>`);
|
|
@@ -2638,9 +2612,15 @@
|
|
|
2638
2612
|
}
|
|
2639
2613
|
function renderProposal(room, p) {
|
|
2640
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
|
+
: "";
|
|
2641
2620
|
const body =
|
|
2642
2621
|
(p.why ? `<div class="prop-why">${esc(p.why)}</div>` : "") +
|
|
2643
2622
|
`<div class="prop-diff">${proposalDiffHtml(p)}</div>` +
|
|
2623
|
+
windowNote +
|
|
2644
2624
|
(p.warnings && p.warnings.length ? `<ul class="prop-warn">${p.warnings.map((w) => `<li>${esc(w)}</li>`).join("")}</ul>` : "") +
|
|
2645
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>` : "");
|
|
2646
2626
|
const outcome = p.status === "pending" ? undefined : p.status === "applied" ? `applied${p.skipped && p.skipped.length ? ` · not applied: ${p.skipped.join("; ")}` : ""}` : "rejected";
|
|
@@ -3072,11 +3052,11 @@
|
|
|
3072
3052
|
const lookId = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
3073
3053
|
const look = TOKENS.looks[lookId];
|
|
3074
3054
|
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("");
|
|
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("");
|
|
3076
3056
|
const groups = [...new Set(TOKENS.adjustables.map((f) => f.group))];
|
|
3077
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("");
|
|
3078
3058
|
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.")}
|
|
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.")}
|
|
3080
3060
|
<div class="look-adjust" id="sp-look-adjust">
|
|
3081
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>
|
|
3082
3062
|
<p class="hint">Kept for this look alone. The sample follows as you pick; the window follows on save.</p>
|
|
@@ -3275,9 +3255,56 @@
|
|
|
3275
3255
|
input.value = b.dataset.look;
|
|
3276
3256
|
fillRows(b.dataset.look);
|
|
3277
3257
|
paintLookPreview();
|
|
3258
|
+
paintOwnRow();
|
|
3278
3259
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3279
3260
|
}),
|
|
3280
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
|
+
});
|
|
3281
3308
|
looksBox.querySelectorAll('[data-ui="adjust-row"] input').forEach((i) => i.addEventListener("input", paintLookPreview));
|
|
3282
3309
|
looksBox.addEventListener("click", (e) => {
|
|
3283
3310
|
const back = e.target.closest && e.target.closest('[data-ui="adjust-row"] .back');
|
|
@@ -4199,7 +4226,7 @@
|
|
|
4199
4226
|
els.pfDialog.addEventListener("cancel", (event) => event.preventDefault());
|
|
4200
4227
|
function offerLook() {
|
|
4201
4228
|
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("");
|
|
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("");
|
|
4203
4230
|
openDialog($("#look-dialog"));
|
|
4204
4231
|
}
|
|
4205
4232
|
$("#look-dialog-cards").addEventListener("click", async (e) => {
|
|
@@ -4274,10 +4301,47 @@
|
|
|
4274
4301
|
for (const name of CUSTOM_VARS) el.style.removeProperty(name);
|
|
4275
4302
|
for (const [name, value] of Object.entries(customVars(values || {}))) el.style.setProperty(name, value);
|
|
4276
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
|
+
}
|
|
4277
4340
|
let appliedScheme = null;
|
|
4278
4341
|
function applyAppearance() {
|
|
4279
4342
|
const a = (state.settings || {}).appearance || {};
|
|
4280
4343
|
const root = document.documentElement;
|
|
4344
|
+
delete root.dataset.tried;
|
|
4281
4345
|
root.style.setProperty("--fs-scale", String((a.chatFontSize || 14.5) / 14.5));
|
|
4282
4346
|
const look = TOKENS.looks[a.look] ? a.look : TOKENS.current.id;
|
|
4283
4347
|
if (look === TOKENS.current.id) delete root.dataset.look;
|
|
@@ -4295,6 +4359,7 @@
|
|
|
4295
4359
|
if (frame) frame.content = TOKENS.looks[look].elements.browser.themeColor;
|
|
4296
4360
|
applyCustomVars(root, (a.custom || {})[look] || {});
|
|
4297
4361
|
document.querySelectorAll("#participants .avatar .life").forEach((ring) => setLifePath(ring, ring.parentElement));
|
|
4362
|
+
refreshTryButtons();
|
|
4298
4363
|
}
|
|
4299
4364
|
|
|
4300
4365
|
let hubIdentity = null;
|
|
@@ -4307,6 +4372,8 @@
|
|
|
4307
4372
|
}
|
|
4308
4373
|
if (identity) hubIdentity = identity;
|
|
4309
4374
|
state.settings = snapshot.settings;
|
|
4375
|
+
state.looks = snapshot.looks || [];
|
|
4376
|
+
registerLooks(state.looks);
|
|
4310
4377
|
applyAppearance();
|
|
4311
4378
|
state.update = snapshot.update || null;
|
|
4312
4379
|
renderUpdatePop();
|
|
@@ -4485,6 +4552,13 @@
|
|
|
4485
4552
|
if (state.view === "skills" && !editingInDetails()) renderSkillsPage();
|
|
4486
4553
|
if (state.detailsOpen && !editingInDetails()) renderDetails();
|
|
4487
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
|
+
},
|
|
4488
4562
|
settings: (m) => {
|
|
4489
4563
|
state.settings = m.settings;
|
|
4490
4564
|
applyAppearance();
|