viberoom 0.5.7 โ†’ 0.5.9

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/ui/app.js CHANGED
@@ -162,38 +162,18 @@
162
162
  return room.messages.some((m) => m.streaming && m.from === p.id) ? "writing" : "thinking";
163
163
  }
164
164
  const CHAT_EMOJI = ["๐Ÿ˜€", "๐Ÿ˜„", "๐Ÿ˜‚", "๐Ÿ™‚", "๐Ÿ˜‰", "๐Ÿ˜", "๐Ÿค”", "๐Ÿ˜Ž", "๐Ÿฅณ", "๐Ÿ˜…", "๐Ÿ˜ข", "๐Ÿ˜ก", "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘‹", "๐Ÿ™", "๐Ÿ‘", "๐Ÿ’ช", "๐Ÿ”ฅ", "โœจ", "๐ŸŽ‰", "โค๏ธ", "๐Ÿ’œ", "โœ…", "โŒ", "โš ๏ธ", "๐Ÿ’ก", "๐Ÿš€", "๐Ÿ›", "๐Ÿค–", "๐Ÿคซ", "โ˜•"];
165
- const ROOM_EMOJI = ["๐ŸŽญ", "๐Ÿš€", "๐Ÿงช", "๐Ÿ› ๏ธ", "๐ŸŽจ", "๐Ÿ“š", "๐Ÿง ", "๐Ÿ’ฌ", "๐Ÿ”ฌ", "๐ŸŽฏ", "๐Ÿ™", "โ˜•", "๐ŸŒˆ", "๐Ÿ—๏ธ", "๐ŸŽฎ", "๐Ÿ”ฅ", "๐Ÿงฉ", "๐Ÿ“ˆ", "๐Ÿ—บ๏ธ", "๐ŸŽง", "๐ŸŒฑ", "๐Ÿ ", "๐Ÿ›ธ", "๐Ÿงญ"];
165
+ const ROOM_EMOJI = [
166
+ "๐ŸŽญ", "๐Ÿš€", "๐Ÿงช", "๐Ÿ› ๏ธ", "๐ŸŽจ", "๐Ÿ“š", "๐Ÿง ", "๐Ÿ’ฌ", "๐Ÿ”ฌ", "๐ŸŽฏ", "๐Ÿ™", "โ˜•", "๐ŸŒˆ", "๐Ÿ—๏ธ", "๐ŸŽฎ", "๐Ÿ”ฅ", "๐Ÿงฉ", "๐Ÿ“ˆ", "๐Ÿ—บ๏ธ", "๐ŸŽง", "๐ŸŒฑ", "๐Ÿ ", "๐Ÿ›ธ", "๐Ÿงญ",
167
+ "๐Ÿง‘โ€๐Ÿ’ป", "โš’๏ธ", "๐Ÿ”จ", "๐Ÿ”ง", "๐Ÿ”ฉ", "โš™๏ธ", "๐Ÿงฐ", "๐Ÿช›", "๐Ÿงฑ", "๐Ÿญ", "๐Ÿ”Œ", "๐Ÿ–ฅ๏ธ", "๐Ÿ’ป", "โŒจ๏ธ", "๐Ÿค–", "๐Ÿ›", "๐Ÿž", "โšก", "๐Ÿ”‹",
168
+ "๐Ÿ“Š", "๐Ÿ“‰", "๐Ÿงฎ", "๐Ÿ”", "๐Ÿ”ญ", "๐Ÿงฌ", "โš—๏ธ", "๐Ÿงฒ", "๐Ÿ“ก", "๐Ÿ›ฐ๏ธ", "๐Ÿ—„๏ธ", "๐Ÿ’พ", "๐Ÿ—ƒ๏ธ", "๐ŸŒ", "๐Ÿ”—", "โ˜๏ธ",
169
+ "โœ๏ธ", "๐Ÿ“", "๐Ÿ“–", "๐Ÿ“ฐ", "๐Ÿ“œ", "๐Ÿ“Ž", "๐Ÿ“Œ", "๐Ÿ—‚๏ธ", "๐Ÿท๏ธ", "โœ‰๏ธ", "๐Ÿ“ฃ", "๐Ÿ—ฃ๏ธ", "๐Ÿค", "๐Ÿ‘ฅ",
170
+ "๐Ÿ–Œ๏ธ", "๐Ÿ–ผ๏ธ", "๐Ÿ“ท", "๐ŸŽฌ", "๐ŸŽฅ", "๐ŸŽต", "๐ŸŽน", "๐ŸŽธ", "๐ŸŽค", "๐ŸŽฒ", "โ™Ÿ๏ธ", "๐Ÿงธ", "๐ŸŽช", "๐ŸŽ",
171
+ "๐Ÿ“…", "โฐ", "โณ", "๐Ÿ—ณ๏ธ", "โš–๏ธ", "๐Ÿงพ", "๐Ÿ’ฐ", "๐Ÿ“ฆ", "๐Ÿšš", "๐Ÿ›’", "๐Ÿฆ", "๐Ÿข", "๐ŸŽ“", "๐Ÿซ", "๐Ÿ", "๐Ÿ†", "๐Ÿ’Ž",
172
+ "๐Ÿ”", "๐Ÿ”‘", "๐Ÿ›ก๏ธ", "๐Ÿšจ", "๐Ÿšฆ", "๐Ÿงฏ", "๐Ÿฉบ", "๐Ÿงน", "โ™ป๏ธ", "๐Ÿง‘โ€๐Ÿณ", "๐Ÿง‘โ€๐Ÿ”ฌ", "๐Ÿง‘โ€๐ŸŽจ", "๐Ÿง‘โ€๐Ÿซ", "๐Ÿง‘โ€๐Ÿš€", "๐Ÿ•ต๏ธ", "๐Ÿง™",
173
+ "๐Ÿฆ‰", "๐ŸฆŠ", "๐Ÿผ", "๐Ÿ", "๐Ÿข", "๐Ÿฌ", "๐Ÿฆ„", "๐Ÿฒ", "๐ŸŒ", "๐ŸŒ™", "โญ", "๐ŸŒŠ", "๐Ÿ”๏ธ", "๐Ÿ๏ธ", "๐ŸŒฒ", "๐Ÿ€", "๐ŸŒธ", "๐Ÿ•", "๐ŸŽ", "โœจ", "๐Ÿ’ก", "๐Ÿ”ฎ", "๐Ÿช„", "โค๏ธ",
174
+ ];
166
175
  function emojiGrid(list, current, onPick) {
167
- const wrap = document.createElement("div");
168
- wrap.className = "avatar-picker";
169
- const render = (value) => {
170
- wrap.innerHTML = "";
171
- if (current !== null && current !== undefined) {
172
- const none = document.createElement("button");
173
- none.type = "button";
174
- none.className = "none" + (!value ? " selected" : "");
175
- none.textContent = "โ€”";
176
- none.title = "No emoji";
177
- none.addEventListener("click", () => {
178
- onPick("");
179
- render("");
180
- });
181
- wrap.appendChild(none);
182
- }
183
- for (const e of list) {
184
- const b = document.createElement("button");
185
- b.type = "button";
186
- b.textContent = e;
187
- b.className = e === value ? "selected" : "";
188
- b.addEventListener("click", () => {
189
- onPick(e);
190
- render(e);
191
- });
192
- wrap.appendChild(b);
193
- }
194
- };
195
- render(current || "");
196
- return wrap;
176
+ return window.Avatars.searchableGrid(list, current, onPick, current !== null && current !== undefined ? { label: "โ€”", title: "No emoji" } : null);
197
177
  }
198
178
  const CLAMP_CHARS = 700;
199
179
 
@@ -627,23 +607,30 @@
627
607
  function sectionTitle(iconName, text) {
628
608
  return `<h4>${ic(iconName)}${text}</h4>`;
629
609
  }
630
- function saveRow(id) {
631
- return `<div class="save-row"><button class="btn sm primary save" id="${id}" disabled>Save</button></div>`;
632
- }
633
610
  function geek(id, bodyHtml, hint) {
634
611
  return `<details class="geek" id="${id}"><summary>${ic("geek")}for geeks${hint ? `<span class="g-hint">${hint}</span>` : ""}<span class="chev">${ic("down")}</span></summary><div class="geek-body">${bodyHtml}</div></details>`;
635
612
  }
613
+ const SAVED_MARK_MS = 2000;
636
614
  const recentlySaved = new Map();
637
- function bindSave(container, button, onSave) {
638
- if (!container || !button) return;
615
+ function markSaved(fieldId, ms) {
616
+ const el = fieldId && document.getElementById(fieldId);
617
+ const host = el && el.closest(".field, .switch");
618
+ const label = host && host.querySelector(":scope > .label");
619
+ if (!label) return;
620
+ const old = label.querySelector(".fsaved");
621
+ if (old) old.remove();
622
+ label.insertAdjacentHTML("beforeend", `<span class="fsaved">${ic("check")}Saved</span>`);
623
+ const mark = label.lastElementChild;
624
+ setTimeout(() => mark.remove(), ms == null ? SAVED_MARK_MS : ms);
625
+ }
626
+ function bindSave(container, onSave) {
627
+ if (!container) return;
639
628
  let dirty = false;
640
629
  let saving = false;
641
630
  let again = false;
631
+ let lastField = null;
642
632
  const arm = () => {
643
633
  dirty = true;
644
- button.disabled = false;
645
- button.classList.remove("saved");
646
- button.textContent = "Save";
647
634
  };
648
635
  const save = async () => {
649
636
  if (!dirty) return;
@@ -653,18 +640,16 @@
653
640
  }
654
641
  saving = true;
655
642
  dirty = false;
656
- button.disabled = true;
657
- button.classList.add("loading");
643
+ const field = lastField;
644
+ lastField = null;
658
645
  try {
659
646
  await onSave();
660
- button.classList.remove("loading");
661
- button.classList.add("saved");
662
- button.innerHTML = `${ic("check")} Saved`;
663
- if (button.id) recentlySaved.set(button.id, Date.now());
647
+ if (field) {
648
+ recentlySaved.set(field, Date.now());
649
+ markSaved(field);
650
+ }
664
651
  } catch (e) {
665
652
  dirty = true;
666
- button.classList.remove("loading");
667
- button.disabled = false;
668
653
  showError(e);
669
654
  }
670
655
  saving = false;
@@ -673,14 +658,14 @@
673
658
  save();
674
659
  }
675
660
  };
676
- if (button.id && Date.now() - (recentlySaved.get(button.id) || 0) < 5000) {
677
- button.disabled = true;
678
- button.classList.add("saved");
679
- button.innerHTML = `${ic("check")} Saved`;
661
+ for (const [id, at] of recentlySaved) {
662
+ const left = SAVED_MARK_MS - (Date.now() - at);
663
+ if (left > 0 && container.querySelector(`#${CSS.escape(id)}`)) markSaved(id, left);
680
664
  }
681
665
  container.addEventListener("input", arm);
682
- container.addEventListener("change", () => {
666
+ container.addEventListener("change", (e) => {
683
667
  arm();
668
+ if (e.target.id) lastField = e.target.id;
684
669
  save();
685
670
  });
686
671
  container.addEventListener("keydown", (e) => {
@@ -692,11 +677,10 @@
692
677
  }
693
678
  });
694
679
  container.addEventListener("focusout", (e) => {
695
- if (e.target.isContentEditable) save();
696
- });
697
- button.addEventListener("click", () => {
698
- dirty = true;
699
- save();
680
+ if (e.target.isContentEditable) {
681
+ if (e.target.id) lastField = e.target.id;
682
+ save();
683
+ }
700
684
  });
701
685
  }
702
686
  function roomHue(room) {
@@ -1144,7 +1128,7 @@
1144
1128
  const rows = new Map([...els.participants.children].map((li) => [li.dataset.id, li]));
1145
1129
  for (const p of ordered) {
1146
1130
  let li = rows.get(p.id);
1147
- const selected = (state.selection.kind === "participant" && state.selection.id === p.id) || (p.kind === "human" && state.selection.kind === "me" && state.detailsOpen);
1131
+ const selected = state.detailsOpen && ((state.selection.kind === "participant" && state.selection.id === p.id) || (p.kind === "human" && state.selection.kind === "me"));
1148
1132
  const asleep = p.kind === "agent" && (p.status === "offline" || p.status === "left");
1149
1133
  const unstaffed = p.kind === "agent" && p.status === "unstaffed";
1150
1134
  const shown = shownStatus(room, p);
@@ -1156,7 +1140,7 @@
1156
1140
  : asleep
1157
1141
  ? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
1158
1142
  : p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${shown}">${p.status === "thinking" ? '<span class="dot"></span>' : ""}${STATUS_LABEL[shown] || shown}</span>` : "";
1159
- const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true });
1143
+ const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true, muted: p.muted });
1160
1144
  const statusClass = p.kind === "agent" ? `avatar-status status-${esc(shown || "idle")}` : "";
1161
1145
  const bodyHtml = `<div class="p-body">
1162
1146
  <div class="p-name"><span>${esc(p.name)}</span>${p.muted ? '<span class="badge muted">muted</span>' : ""}${status}</div>
@@ -1630,6 +1614,7 @@
1630
1614
  if (!placed.has(seq)) placeInList(dividerElement(agents));
1631
1615
  }
1632
1616
  for (const perm of room.permissions) renderPermission(room, perm);
1617
+ for (const prop of room.proposals || []) renderProposal(room, prop);
1633
1618
  refreshSeen(room);
1634
1619
  scrollToBottom();
1635
1620
  renderTimeline();
@@ -1638,6 +1623,10 @@
1638
1623
  function upsertMessage(roomId, m) {
1639
1624
  const room = state.rooms.get(roomId);
1640
1625
  if (!room) return;
1626
+ if (m.from === "human" && !m.pending && !room.messages.some((x) => x.id === m.id)) {
1627
+ const local = room.messages.find((x) => x.pending && x.from === "human" && x.text === m.text);
1628
+ if (local) adoptLocalMessage(roomId, local.id, m.id);
1629
+ }
1641
1630
  const idx = room.messages.findIndex((x) => x.id === m.id);
1642
1631
  const wasFinal = idx >= 0 && !room.messages[idx].streaming;
1643
1632
  if (idx >= 0) {
@@ -1785,6 +1774,16 @@
1785
1774
  editEls.rewrite.addEventListener("click", () => editRequest && submitEdit(editRequest.room, editRequest.m, editRequest.next, "rewrite"));
1786
1775
 
1787
1776
 
1777
+ function placeCard(card, room, ts) {
1778
+ let anchor = null;
1779
+ for (const m of room.messages) {
1780
+ if (m.ts > ts) break;
1781
+ const el = els.messages.querySelector(`.msg[data-id="${CSS.escape(m.id)}"]`);
1782
+ if (el) anchor = el;
1783
+ }
1784
+ if (anchor) anchor.insertAdjacentElement("afterend", card);
1785
+ else els.messages.appendChild(card);
1786
+ }
1788
1787
  function renderPermission(room, perm) {
1789
1788
  const p = findById(room, perm.participantId);
1790
1789
  const card = document.createElement("div");
@@ -1811,8 +1810,9 @@
1811
1810
  actions.appendChild(cancel);
1812
1811
  const draft = [...room.messages].reverse().find((m) => m.streaming && m.from === perm.participantId);
1813
1812
  const host = draft ? els.messages.querySelector(`.msg[data-id="${draft.id}"] .perms`) : null;
1814
- (host || els.messages).appendChild(card);
1815
- scrollToBottom();
1813
+ if (host) host.appendChild(card);
1814
+ else placeCard(card, room, perm.ts || Date.now());
1815
+ if (stuck) scrollToBottom();
1816
1816
  }
1817
1817
  function resolvePermissionCard(key, optionId) {
1818
1818
  const card = document.querySelector(`.perm[data-key="${key}"]`);
@@ -1821,6 +1821,64 @@
1821
1821
  card.querySelector(".perm-actions").innerHTML = `<span class="perm-result">${optionId ? `chosen: ${esc(optionId)}` : "dismissed"}</span>`;
1822
1822
  }
1823
1823
 
1824
+ function proposalValue(v) {
1825
+ if (v === null || v === undefined || v === "") return "(empty)";
1826
+ if (typeof v === "object") return v.mode === "fixed" ? v.language : v.mode || JSON.stringify(v);
1827
+ return String(v);
1828
+ }
1829
+ function proposalDiffHtml(p) {
1830
+ const rows = [];
1831
+ for (const c of p.settings || []) {
1832
+ if (c.key === "customRules") {
1833
+ const before = String(c.from || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
1834
+ const after = String(c.to || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
1835
+ const lines = [...before.filter((l) => !after.includes(l)).map((l) => `<div class="prop-line del">โˆ’ ${esc(l)}</div>`), ...after.filter((l) => !before.includes(l)).map((l) => `<div class="prop-line add">+ ${esc(l)}</div>`)];
1836
+ rows.push(`<div class="prop-row"><b>Room rules</b>${lines.join("")}</div>`);
1837
+ } 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>`);
1838
+ }
1839
+ for (const v of p.vibemates || []) {
1840
+ 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>`);
1841
+ 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>`);
1842
+ }
1843
+ return rows.join("");
1844
+ }
1845
+ function renderProposal(room, p) {
1846
+ const existing = els.messages.querySelector(`.proposal[data-key="${p.key}"]`);
1847
+ const card = existing || document.createElement("div");
1848
+ card.className = `perm proposal${p.status !== "pending" ? " resolved" : ""}`;
1849
+ card.dataset.key = p.key;
1850
+ card.innerHTML = `
1851
+ <div class="perm-title">${ic("pencil")} ${esc(p.participantName)} proposes changes to the room</div>
1852
+ ${p.why ? `<div class="prop-why">${esc(p.why)}</div>` : ""}
1853
+ <div class="prop-diff">${proposalDiffHtml(p)}</div>
1854
+ ${p.warnings && p.warnings.length ? `<ul class="prop-warn">${p.warnings.map((w) => `<li>${esc(w)}</li>`).join("")}</ul>` : ""}
1855
+ ${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>` : ""}
1856
+ <div class="perm-actions"></div>`;
1857
+ const actions = card.querySelector(".perm-actions");
1858
+ if (p.status === "pending") {
1859
+ const apply = document.createElement("button");
1860
+ apply.className = "perm-btn kind-allow_once";
1861
+ apply.textContent = "Apply";
1862
+ apply.addEventListener("click", () => post(roomApi(`/proposals/${encodeURIComponent(p.key)}`), { accept: true }).catch(showError));
1863
+ const reject = document.createElement("button");
1864
+ reject.className = "perm-btn kind-reject_once";
1865
+ reject.textContent = "Reject";
1866
+ reject.addEventListener("click", () => post(roomApi(`/proposals/${encodeURIComponent(p.key)}`), { accept: false }).catch(showError));
1867
+ actions.append(apply, reject);
1868
+ } else actions.innerHTML = `<span class="perm-result">${p.status === "applied" ? `applied${p.skipped && p.skipped.length ? ` ยท not applied: ${esc(p.skipped.join("; "))}` : ""}` : "rejected"}</span>`;
1869
+ if (!existing) {
1870
+ placeCard(card, room, p.ts || Date.now());
1871
+ if (stuck) scrollToBottom();
1872
+ }
1873
+ }
1874
+ function resolveProposalCard(room, key, status) {
1875
+ const p = (room.proposals || []).find((x) => x.key === key);
1876
+ if (p) {
1877
+ p.status = status;
1878
+ renderProposal(room, p);
1879
+ }
1880
+ }
1881
+
1824
1882
 
1825
1883
  const DETAILS_MIN = 320;
1826
1884
  const DETAILS_MAX = 760;
@@ -1879,7 +1937,7 @@
1879
1937
  renderRoomPanel(room);
1880
1938
  }
1881
1939
  function profileHeader(p) {
1882
- return `${avatar(p, 76, { vendor: true, status: true })}
1940
+ return `${avatar(p, 76, { vendor: true, status: true, muted: p.muted })}
1883
1941
  <h3>${esc(p.name)}</h3>
1884
1942
  <div class="tagline">${esc(p.tagline || "no vibersona")}</div>
1885
1943
  <div class="badges"><span class="badge">${esc(p.agentVendor || p.agentType || "vibemate")}</span><span class="badge status-${p.status}">${STATUS_LABEL[p.status] || p.status}</span>${p.muted ? '<span class="badge muted">muted</span>' : ""}</div>`;
@@ -1906,7 +1964,7 @@
1906
1964
  </div>
1907
1965
  <div class="action-row">
1908
1966
  <button class="action" data-act="mention"><span class="ico">${ic("at")}</span>Mention</button>
1909
- <button class="action" data-act="${p.muted ? "unmute" : "mute"}"><span class="ico">${ic(p.muted ? "bell" : "bell-off")}</span>${p.muted ? "Unmute" : "Mute"}</button>
1967
+ <button class="action" data-act="${p.muted ? "unmute" : "mute"}"><span class="ico">${ic(p.muted ? "mute" : "unmute")}</span>${p.muted ? "Unmute" : "Mute"}</button>
1910
1968
  ${offline ? `<button class="action" data-act="reconnect"><span class="ico">${ic("refresh")}</span>Reconnect</button>` : `<button class="action" data-act="cancel" ${p.status !== "thinking" ? "disabled" : ""}><span class="ico">${ic("stop")}</span>Stop</button>`}
1911
1969
  <button class="action danger" data-act="remove"><span class="ico">${ic("trash")}</span>Remove</button>
1912
1970
  </div>
@@ -1916,25 +1974,23 @@
1916
1974
  ${field("Vibersona", `<input type="text" id="pp-tagline" maxlength="80" value="${esc(p.tagline || "")}" placeholder="a few words under the vibename">`, "Shown under the vibename.", "Everyone in the room sees it: you, and the other vibemates in their roster.")}
1917
1975
  ${field("Vibeface", `<div id="pp-avatar-picker"></div><input type="text" id="pp-avatar" maxlength="8" value="${esc(p.avatar || "")}" placeholder="custom emoji (optional)">`)}
1918
1976
  ${field("Vibio", `<textarea id="pp-role" rows="5" maxlength="4000" placeholder="who it is, how it speaks, what it cares about">${esc(p.role || "")}</textarea>`, "Only this vibemate reads it.", "Reaches the vibemate as refreshed instructions in its brief on its next turn; its memory is kept. The other participants never see it.")}
1919
- ${saveRow("pp-save")}
1920
1977
  </div>
1921
1978
  ${p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<p class="hint" style="color:var(--danger);margin:0 4px 10px">${esc(p.statusDetail)}</p>` : ""}
1979
+ <div class="section" id="pp-engine">
1980
+ ${sectionTitle("spark", "Coding agent")}
1981
+ <p class="hint">What runs ${esc(p.name)}${rec ? `: ${esc(rec.vendor)}` : ""}. Its model, how hard it thinks, what it may do without asking.${geekTip("These options come from the coding agent itself: the hub lists the ones it offers and sets your pick on its running session, so a change takes effect from the next turn, without restarting it or losing what it remembers.")}</p>
1982
+ <div id="pp-config"></div>
1983
+ </div>
1922
1984
  ${geek(
1923
1985
  "pp-geek",
1924
1986
  `<div class="section" id="pp-skills-section">
1925
1987
  ${sectionTitle("skills", "Skills")}
1926
1988
  <div class="check-list" id="pp-skills"></div>
1927
1989
  <p class="hint" style="margin-top:8px">What this vibemate can load on request.${geekTip(`Listed in this vibemate's brief by name and description; the text arrives when you write /name or when the vibemate loads it. ${esc(skillChannelText(p))}`)}</p>
1928
- ${saveRow("pp-skills-save")}
1929
1990
  </div>
1930
1991
  <div class="section" id="pp-timing">
1931
1992
  ${sectionTitle("bolt", "Timing")}
1932
1993
  ${field("Reply delay override, seconds", `<input type="number" id="pp-delay" min="0" max="120" step="0.5" value="${p.replyDelay ?? ""}" 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.")}
1933
- ${saveRow("pp-delay-save")}
1934
- </div>
1935
- <div class="section">
1936
- ${sectionTitle("link", "Session")}
1937
- <div id="pp-config"></div>
1938
1994
  </div>
1939
1995
  <div class="section danger">
1940
1996
  ${sectionTitle("bolt", "Respawn")}
@@ -1956,7 +2012,7 @@
1956
2012
  <span>Adapter</span><span>${esc(rec ? rec.label : p.agentLabel || "")}${p.agentInfo && p.agentInfo.version ? ` ${esc(p.agentInfo.version)}` : ""}</span>
1957
2013
  </div>
1958
2014
  </div>`,
1959
- "skills, timing, session, stats",
2015
+ "skills, timing, stats",
1960
2016
  )}`;
1961
2017
  wireDetailsClose();
1962
2018
  const respawnBtn = els.detailsInner.querySelector('button[data-act="respawn"]');
@@ -1987,9 +2043,9 @@
1987
2043
  }),
1988
2044
  );
1989
2045
  renderSkillChecks($("#pp-skills"), p.skills || []);
1990
- bindSave($("#pp-skills-section"), $("#pp-skills-save"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { skills: checkedSkills($("#pp-skills")) }));
1991
- bindSave($("#pp-timing"), $("#pp-delay-save"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { replyDelay: $("#pp-delay").value === "" ? null : Number($("#pp-delay").value) }));
1992
- bindSave($("#pp-persona"), $("#pp-save"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { name: $("#pp-name").value, tagline: $("#pp-tagline").value, role: $("#pp-role").value, avatar: $("#pp-avatar").value }));
2046
+ bindSave($("#pp-skills-section"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { skills: checkedSkills($("#pp-skills")) }));
2047
+ bindSave($("#pp-timing"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { replyDelay: $("#pp-delay").value === "" ? null : Number($("#pp-delay").value) }));
2048
+ bindSave($("#pp-persona"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { name: $("#pp-name").value, tagline: $("#pp-tagline").value, role: $("#pp-role").value, avatar: $("#pp-avatar").value }));
1993
2049
  renderConfig($("#pp-config"), p, offline);
1994
2050
  }
1995
2051
 
@@ -2051,7 +2107,6 @@
2051
2107
  ${field("Vibename", `<input type="text" id="me-name" maxlength="24" value="${esc(s.humanName || "")}">`, "How you appear in every room.")}
2052
2108
  ${field("Vibeface", `<div id="me-avatar-picker"></div><input type="text" id="me-avatar" maxlength="8" value="${esc(s.humanAvatar || "")}" placeholder="custom emoji (optional)">`)}
2053
2109
  ${field("Your vibe line", `<textarea id="me-desc" rows="3" maxlength="200" placeholder="e.g. software engineer, curious about agent protocols; likes short answers">${esc(s.humanDescription || "")}</textarea>`, "A sentence or two about you.", "The vibemates get it in every room's brief, unless a room adds its own line or replaces it (below, when you are in a room).")}
2054
- ${saveRow("me-save")}
2055
2110
  </div>
2056
2111
  ${
2057
2112
  rs
@@ -2059,7 +2114,6 @@
2059
2114
  ${sectionTitle("chat", "In this room")}
2060
2115
  ${field("What vibemates get about you here", `<select id="hp-mode"><option value="inherit"${rs.humanDescriptionMode === "inherit" ? " selected" : ""}>Your vibe line</option><option value="append"${rs.humanDescriptionMode === "append" ? " selected" : ""}>Your vibe line + this room's</option><option value="override"${rs.humanDescriptionMode === "override" ? " selected" : ""}>Only this room's line</option><option value="none"${rs.humanDescriptionMode === "none" ? " selected" : ""}>Nothing about me in this room</option></select>`)}
2061
2116
  ${field("This room's line about you", `<textarea id="hp-desc" rows="3" maxlength="200" placeholder="e.g. host of this session, product owner">${esc(rs.humanDescription || "")}</textarea>`)}
2062
- ${saveRow("hp-save")}
2063
2117
  </div>`
2064
2118
  : ""
2065
2119
  }
@@ -2075,8 +2129,8 @@
2075
2129
  $("#me-avatar").dispatchEvent(new Event("change", { bubbles: true }));
2076
2130
  }),
2077
2131
  );
2078
- bindSave($("#me-vibe"), $("#me-save"), () => post("/api/settings", { humanName: $("#me-name").value, humanAvatar: $("#me-avatar").value, humanDescription: $("#me-desc").value }));
2079
- bindSave($("#me-room"), $("#hp-save"), () => post(roomApi("/settings"), { humanDescriptionMode: $("#hp-mode").value, humanDescription: $("#hp-desc").value }));
2132
+ bindSave($("#me-vibe"), () => post("/api/settings", { humanName: $("#me-name").value, humanAvatar: $("#me-avatar").value, humanDescription: $("#me-desc").value }));
2133
+ bindSave($("#me-room"), () => post(roomApi("/settings"), { humanDescriptionMode: $("#hp-mode").value, humanDescription: $("#hp-desc").value }));
2080
2134
  $("#me-erase").addEventListener("click", openEraseDialog);
2081
2135
  }
2082
2136
 
@@ -2138,7 +2192,7 @@
2138
2192
  </div>`,
2139
2193
  "tools, hops, referee, briefs",
2140
2194
  )}
2141
- <div class="save-row" style="margin-top:10px"><span class="hint">The vibemates get the changes on their next turn.</span><button class="btn sm primary save" id="rp-save" disabled>Save</button></div>
2195
+ <div class="save-row" style="margin-top:10px"><span class="hint">The vibemates get the changes on their next turn.</span></div>
2142
2196
  </div>
2143
2197
  <div class="section danger" style="margin-top:12px">
2144
2198
  ${sectionTitle("alert", "Danger zone")}
@@ -2151,7 +2205,7 @@
2151
2205
  $("#rp-emoji-picker").appendChild(
2152
2206
  emojiGrid(ROOM_EMOJI, rs.emoji || "", (emoji) => {
2153
2207
  $("#rp-emoji").value = emoji;
2154
- $("#rp-emoji").dispatchEvent(new Event("input", { bubbles: true }));
2208
+ $("#rp-emoji").dispatchEvent(new Event("change", { bubbles: true }));
2155
2209
  }),
2156
2210
  );
2157
2211
  $("#rp-dir-browse").addEventListener("click", () => openFolderPicker($("#rp-dir").value, (dir) => {
@@ -2159,7 +2213,7 @@
2159
2213
  $("#rp-dir").dispatchEvent(new Event("change", { bubbles: true }));
2160
2214
  }));
2161
2215
  $("#rp-template").addEventListener("click", () => openSaveTemplateDialog(room));
2162
- bindSave($("#rp-form"), $("#rp-save"), async () => {
2216
+ bindSave($("#rp-form"), async () => {
2163
2217
  const name = $("#rp-name").value;
2164
2218
  if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
2165
2219
  const dir = $("#rp-dir").value.trim();
@@ -2220,7 +2274,7 @@
2220
2274
  installed.map((r) => `<div class="vendor-row">${logo(r)}<span class="vc-name">${esc(r.vendor)}<span class="hint" title="${esc(r.installedAt || "")}">${esc(r.installedAt || "bundled")}</span></span><span class="badge status-idle"><span class="dot"></span>installed</span></div>`).join("") +
2221
2275
  missing.map((r) => `<div class="vendor-row" style="opacity:.75">${logo(r)}<span class="vc-name">${esc(r.vendor)}<span class="hint">${esc(r.installHint || r.unavailableReason || "")}</span></span><span class="badge status-offline">not installed</span></div>`).join("");
2222
2276
  els.pageInner.innerHTML = `
2223
- <div class="page-head"><div><h1>Settings</h1><div class="hint">${state.version ? `${esc(state.version.name)} ${esc(state.version.version)} ยท hub built ${esc(new Date(state.version.build).toLocaleString())}` : "hub build unknown (older hub process; run viberoom again to replace it)"}</div></div><div class="row-btns"><button class="btn primary save" id="sp-save" disabled>Save</button></div></div>
2277
+ <div class="page-head"><div><h1>Settings</h1><div class="hint">${state.version ? `${esc(state.version.name)} ${esc(state.version.version)} ยท hub built ${esc(new Date(state.version.build).toLocaleString())}` : "hub build unknown (older hub process; run viberoom again to replace it)"}</div></div></div>
2224
2278
  <div id="sp-form">
2225
2279
  <div class="page-cols">
2226
2280
  <div>
@@ -2314,7 +2368,7 @@
2314
2368
  editorSection.querySelectorAll(".editor-modes .chip-btn").forEach((x) => x.classList.toggle("on", x === b));
2315
2369
  const input = $("#sp-editor-mode");
2316
2370
  input.value = b.dataset.mode;
2317
- input.dispatchEvent(new Event("input", { bubbles: true }));
2371
+ input.dispatchEvent(new Event("change", { bubbles: true }));
2318
2372
  showEditorCmd();
2319
2373
  }),
2320
2374
  );
@@ -2337,7 +2391,7 @@
2337
2391
  diagramSection.querySelectorAll(".diagram-presets .chip-btn").forEach((x) => x.classList.toggle("on", x === b));
2338
2392
  const input = $("#sp-diagram-preset");
2339
2393
  input.value = b.dataset.preset;
2340
- input.dispatchEvent(new Event("input", { bubbles: true }));
2394
+ input.dispatchEvent(new Event("change", { bubbles: true }));
2341
2395
  redrawPreview();
2342
2396
  }),
2343
2397
  );
@@ -2368,7 +2422,7 @@
2368
2422
  b.disabled = false;
2369
2423
  b.classList.remove("loading");
2370
2424
  });
2371
- bindSave($("#sp-form"), $("#sp-save"), async () => {
2425
+ bindSave($("#sp-form"), async () => {
2372
2426
  const vendorPresets = {};
2373
2427
  els.pageInner.querySelectorAll("input[data-vendor]").forEach((inp) => {
2374
2428
  vendorPresets[inp.dataset.vendor] = vendorPresets[inp.dataset.vendor] || { model: null, effort: null, mode: null };
@@ -3358,6 +3412,18 @@
3358
3412
  room.permissions = room.permissions.filter((p) => p.key !== event.key);
3359
3413
  if (showing) resolvePermissionCard(event.key, event.optionId);
3360
3414
  return;
3415
+ case "proposal":
3416
+ room.proposals = [...(room.proposals || []), event.proposal];
3417
+ if (showing) renderProposal(room, event.proposal);
3418
+ else toast(`${esc(event.proposal.participantName)} proposes changes to "${room.name}".`, "warn");
3419
+ return;
3420
+ case "proposal.resolved": {
3421
+ const p = (room.proposals || []).find((x) => x.key === event.key);
3422
+ if (p) p.skipped = event.skipped;
3423
+ if (showing) resolveProposalCard(room, event.key, event.status);
3424
+ else if (p) p.status = event.status;
3425
+ return;
3426
+ }
3361
3427
  case "room":
3362
3428
  room.hopLimit = event.hopLimit;
3363
3429
  room.hops = event.hops;
@@ -3700,13 +3766,14 @@
3700
3766
  autosize();
3701
3767
  const local = { id: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, seq: 0, from: "human", fromName: (state.settings || {}).humanName || "You", to: [], toNames: [], text, ts: Date.now(), kind: "chat", pending: true };
3702
3768
  if (shots.length) local.images = shots.map((shot) => ({ file: "", name: shot.name, mimeType: shot.mimeType, bytes: 0, n: shot.n, url: shot.data }));
3769
+ const localId = local.id;
3703
3770
  upsertMessage(room.id, local);
3704
3771
  try {
3705
3772
  const r = await post(roomApi("/send"), { text, images: shots });
3706
- if (r.command) removeMessage(room.id, local.id);
3707
- else adoptLocalMessage(room.id, local.id, r.id);
3773
+ if (r.command) removeMessage(room.id, localId);
3774
+ else adoptLocalMessage(room.id, localId, r.id);
3708
3775
  } catch (error) {
3709
- removeMessage(room.id, local.id);
3776
+ removeMessage(room.id, localId);
3710
3777
  showError(error);
3711
3778
  els.input.value = text;
3712
3779
  pendingShots = shots;
@@ -3715,7 +3782,7 @@
3715
3782
  });
3716
3783
  function adoptLocalMessage(roomId, localId, realId) {
3717
3784
  const room = state.rooms.get(roomId);
3718
- if (!room || !realId) return;
3785
+ if (!room || !realId || localId === realId) return;
3719
3786
  if (room.messages.some((m) => m.id === realId)) return removeMessage(roomId, localId);
3720
3787
  const m = room.messages.find((x) => x.id === localId);
3721
3788
  if (!m) return;
@@ -3723,6 +3790,11 @@
3723
3790
  delete m.pending;
3724
3791
  const el = els.messages.querySelector(`.msg[data-id="${localId}"]`);
3725
3792
  if (el) el.dataset.id = realId;
3793
+ const note = doneNotes.find((n) => n.id === localId);
3794
+ if (note) {
3795
+ note.id = realId;
3796
+ renderDoneNotes();
3797
+ }
3726
3798
  }
3727
3799
 
3728
3800
 
@@ -4347,10 +4419,10 @@
4347
4419
  let shown = 0;
4348
4420
  for (const b of buttons) {
4349
4421
  const draft = room.messages.find((x) => x.from === b.dataset.id && x.streaming);
4350
- const head = draft && els.messages.querySelector(`.msg[data-id="${draft.id}"] .head`);
4422
+ const el = draft && els.messages.querySelector(`.msg[data-id="${draft.id}"]`);
4351
4423
  let show = false;
4352
- if (head) {
4353
- const r = head.getBoundingClientRect();
4424
+ if (el) {
4425
+ const r = el.getBoundingClientRect();
4354
4426
  show = !(r.bottom > box.top && r.top < box.bottom);
4355
4427
  }
4356
4428
  b.hidden = !show;