viberoom 0.5.1 → 0.5.2

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/dist/room.js CHANGED
@@ -1660,6 +1660,7 @@ export class Room extends EventEmitter {
1660
1660
  }
1661
1661
  const startedAt = runtime.turn.startedAt;
1662
1662
  const published = runtime.turn.published;
1663
+ const publishedAt = runtime.turn.publishedAt ?? null;
1663
1664
  runtime.turn = null;
1664
1665
  runtime.turnActive = false;
1665
1666
  this.drafts.delete(draft.id);
@@ -1689,9 +1690,9 @@ export class Room extends EventEmitter {
1689
1690
  this.closeRetry(retry, "the correction turn failed; nothing was posted");
1690
1691
  return null;
1691
1692
  }
1692
- return this.finalizeTurn(participant, runtime, draft, result, Date.now() - startedAt, retry, published);
1693
+ return this.finalizeTurn(participant, runtime, draft, result, Date.now() - startedAt, retry, published, publishedAt);
1693
1694
  }
1694
- finalizeTurn(participant, runtime, draft, result, durationMs, retry, published) {
1695
+ finalizeTurn(participant, runtime, draft, result, durationMs, retry, published, publishedAt = null) {
1695
1696
  participant.status = "idle";
1696
1697
  this.push({ type: "participant", participant });
1697
1698
  const text = draft.text.trim();
@@ -1787,7 +1788,7 @@ export class Room extends EventEmitter {
1787
1788
  durationMs,
1788
1789
  };
1789
1790
  this.commit(message);
1790
- if (this.messages.some((x) => x.kind === "chat" && x.id !== message.id && x.ts > draft.ts)) {
1791
+ if (publishedAt !== null && this.messages.some((x) => x.kind === "chat" && x.id !== message.id && x.ts > publishedAt)) {
1791
1792
  const at = new Date(draft.ts);
1792
1793
  const hhmm = `${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}`;
1793
1794
  this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${Math.round(durationMs / 1000)} s`, "human", false, { refId: message.id, agentId: participant.id });
@@ -1874,6 +1875,7 @@ export class Room extends EventEmitter {
1874
1875
  if (turn.published)
1875
1876
  return;
1876
1877
  turn.published = true;
1878
+ turn.publishedAt = Date.now();
1877
1879
  this.push({ type: "message", message: turn.message });
1878
1880
  }
1879
1881
  onSessionUpdate(id, update) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberoom",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "viberoom: group chat rooms for a human and several coding agents over the Agent Client Protocol",
5
5
  "type": "module",
6
6
  "engines": {
package/ui/app.css CHANGED
@@ -374,7 +374,20 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
374
374
  .tool { display: inline-flex; flex-direction: column; max-width: 100%; min-width: 0; }
375
375
  .tool.open { flex-basis: 100%; }
376
376
  .tool > .chip { font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; text-align: left; }
377
- .tool-body { margin: 4px 0 2px; padding: 8px 10px; background: var(--soft); border-radius: 10px; font-size: 12px; max-height: 340px; overflow: auto; }
377
+ .tool > .chip-completed { --tool-bg: var(--mint); --tool-ink: var(--mint-ink); }
378
+ .tool > .chip-failed { --tool-bg: var(--rose); --tool-ink: var(--rose-ink); }
379
+ .tool > .chip-in_progress { --tool-bg: var(--warm); --tool-ink: var(--warm-ink); }
380
+ .tool > .chip-pending { --tool-bg: var(--lav); --tool-ink: var(--primary); }
381
+ .tool > .chip::after { content: "▾"; margin-left: auto; padding-left: 8px; opacity: 0.55; transition: transform var(--t-fast); }
382
+ .tool.open > .chip { width: 100%; border-radius: 12px 12px 0 0; padding: 6px 12px; white-space: normal; }
383
+ .tool.open > .chip::after { transform: rotate(180deg); }
384
+ .tool.open { --tool-bg: var(--lav); --tool-ink: var(--primary); }
385
+ .tool.open > .chip-completed ~ .tool-body, .tool.open > .chip-completed + .tool-body { --tool-bg: var(--mint); --tool-ink: var(--mint-ink); }
386
+ .tool.open > .chip-failed + .tool-body { --tool-bg: var(--rose); --tool-ink: var(--rose-ink); }
387
+ .tool.open > .chip-in_progress + .tool-body { --tool-bg: var(--warm); --tool-ink: var(--warm-ink); }
388
+ .tool-body { margin: 0; padding: 8px 10px 10px; background: var(--card); border: 2px solid var(--tool-bg, var(--lav)); border-top: 0; border-radius: 0 0 12px 12px; font-size: 12px; max-height: 340px; overflow: auto; }
389
+ .tool-body pre { background: var(--soft); color: var(--ink); padding: 6px 8px; border-radius: 8px; }
390
+ .tool-sec b { color: var(--tool-ink, var(--muted)); }
378
391
  .tool-sec + .tool-sec { margin-top: 8px; }
379
392
  .tool-sec b { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
380
393
  .tool-sec.muted { color: var(--muted); font-style: italic; }
@@ -582,3 +595,4 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
582
595
  .done-notes { flex-direction: column; }
583
596
  .done-note.new { background: var(--card); color: var(--ink-2); }
584
597
  .done-note.new:hover { background: var(--soft); }
598
+ .meta .seen-by { color: var(--primary); font-weight: 800; margin-left: 6px; }
package/ui/app.js CHANGED
@@ -1417,7 +1417,8 @@
1417
1417
  if (m.durationMs) parts.push(`<span title="how long the reply took">${ic("clock")} ${(m.durationMs / 1000).toFixed(1)} s</span>`);
1418
1418
  if (m.usage) parts.push(`<span title="tokens in">${ic("arrow-down")} ${fmtTokens(m.usage.inputTokens)}</span><span title="tokens out">${ic("arrow-up")} ${fmtTokens(m.usage.outputTokens)}</span>${m.usage.cachedWriteTokens ? `<span title="tokens written to the cache">${ic("database")} ${fmtTokens(m.usage.cachedWriteTokens)}</span>` : ""}`);
1419
1419
  meta.innerHTML = parts.join("<span class=\"sep\">·</span>");
1420
- } else if (m.from === "human") refreshSeen(room);
1420
+ fillSeen(el, room, m);
1421
+ } else if (m.from === "human") fillSeen(el, room, m);
1421
1422
  else meta.textContent = "";
1422
1423
  }
1423
1424
 
@@ -1429,23 +1430,47 @@
1429
1430
  return null;
1430
1431
  }
1431
1432
  function seenHtml(room, m) {
1432
- const present = room.participants.filter((p) => p.kind === "agent" && p.status !== "left" && p.status !== "offline");
1433
+ const present = room.participants.filter((p) => p.kind === "agent" && p.status !== "left" && p.status !== "offline" && p.id !== m.from);
1434
+ if (!present.length) return "";
1433
1435
  const seen = present.filter((p) => p.lastSeenSeq != null && p.lastSeenSeq >= m.seq);
1436
+ if (seen.length === present.length) return "";
1434
1437
  if (!seen.length) return `<span class="ticks">✓</span> sent`;
1435
- if (present.length > 1 && seen.length === present.length) return `<span class="ticks">✓✓</span> seen by all`;
1436
1438
  return `<span class="ticks">✓✓</span> seen by ${esc(seen.map((p) => p.name).join(", "))}`;
1437
1439
  }
1440
+ function fillSeen(el, room, m) {
1441
+ if (m.kind !== "chat" || m.streaming) return;
1442
+ const html = seenHtml(room, m);
1443
+ if (m.from === "human") {
1444
+ const meta = el.querySelector(".meta");
1445
+ if (!meta) return;
1446
+ meta.innerHTML = html;
1447
+ meta.classList.toggle("seen", !!html);
1448
+ } else {
1449
+ const meta = el.querySelector(".meta");
1450
+ let span = meta && meta.querySelector(".seen-by");
1451
+ if (!span && meta && html) {
1452
+ span = document.createElement("span");
1453
+ span.className = "seen-by";
1454
+ meta.appendChild(span);
1455
+ }
1456
+ if (span) {
1457
+ span.innerHTML = html;
1458
+ span.hidden = !html;
1459
+ }
1460
+ }
1461
+ }
1438
1462
  function refreshSeen(room) {
1439
- const last = lastHumanMessage(room);
1463
+ const present = room.participants.filter((p) => p.kind === "agent" && p.status !== "left" && p.status !== "offline");
1464
+ const floor = present.length ? Math.min(...present.map((p) => (p.lastSeenSeq == null ? 0 : p.lastSeenSeq))) : Infinity;
1440
1465
  const byId = new Map(room.messages.map((m) => [m.id, m]));
1441
- for (const el of els.messages.querySelectorAll(".msg.mine")) {
1442
- const meta = el.querySelector(".meta");
1443
- if (!meta) continue;
1444
- const isLast = last && el.dataset.id === last.id;
1445
- meta.innerHTML = isLast ? seenHtml(room, last) : "";
1446
- meta.classList.toggle("seen", !!isLast);
1466
+ for (const el of els.messages.querySelectorAll(".msg.mine, .msg.agent")) {
1467
+ const seq = Number(el.dataset.seq);
1468
+ const hasLabel = el.querySelector(".meta.seen, .seen-by:not([hidden])");
1469
+ if (seq <= floor && !hasLabel) continue;
1447
1470
  const m = byId.get(el.dataset.id);
1448
- if (m && (isLast || el.classList.contains("waiting"))) renderWaiting(el, room, m);
1471
+ if (!m) continue;
1472
+ fillSeen(el, room, m);
1473
+ if (m.from === "human" && (seq > floor || el.classList.contains("waiting"))) renderWaiting(el, room, m);
1449
1474
  }
1450
1475
  }
1451
1476