viberoom 0.5.0 → 0.5.1
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 +1 -1
- package/dist/room.js +9 -2
- package/package.json +1 -1
- package/templates/forge-and-lumen/template.json +3 -3
- package/ui/app.css +11 -1
- package/ui/app.js +65 -12
- package/ui/index.html +1 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Open a room, summon the agents you already have, give each one a role, and let t
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="
|
|
17
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A Forge & Lumen room: Forge explains a git command from a screenshot, Lumen adds the part that changes whose problem it is">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
package/dist/room.js
CHANGED
|
@@ -1539,7 +1539,7 @@ export class Room extends EventEmitter {
|
|
|
1539
1539
|
await this.awaitSkillChannel(participant, runtime);
|
|
1540
1540
|
if (!runtime.agent.alive || participant.muted || this.focused)
|
|
1541
1541
|
return;
|
|
1542
|
-
const unreadAll = this.messages.filter((m) => m.kind !== "hidden" && m.seq > runtime.lastSeenSeq && (m.kind === "system" || m.from !== id || m.seq <= runtime.replayOwnUntilSeq));
|
|
1542
|
+
const unreadAll = this.messages.filter((m) => m.kind !== "hidden" && m.audience !== "human" && m.seq > runtime.lastSeenSeq && (m.kind === "system" || m.from !== id || m.seq <= runtime.replayOwnUntilSeq));
|
|
1543
1543
|
if (!unreadAll.some((m) => m.kind === "chat" || m.wakes))
|
|
1544
1544
|
return;
|
|
1545
1545
|
const cap = this.settings.backlogCap;
|
|
@@ -1787,6 +1787,11 @@ export class Room extends EventEmitter {
|
|
|
1787
1787
|
durationMs,
|
|
1788
1788
|
};
|
|
1789
1789
|
this.commit(message);
|
|
1790
|
+
if (this.messages.some((x) => x.kind === "chat" && x.id !== message.id && x.ts > draft.ts)) {
|
|
1791
|
+
const at = new Date(draft.ts);
|
|
1792
|
+
const hhmm = `${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}`;
|
|
1793
|
+
this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${Math.round(durationMs / 1000)} s`, "human", false, { refId: message.id, agentId: participant.id });
|
|
1794
|
+
}
|
|
1790
1795
|
if (retry) {
|
|
1791
1796
|
this.closeRetry(retry, corrections.length ? `corrected reply posted, but it still breaks: ${corrections.map((c) => c.replace(/^reminder:\s*/i, "")).join("; ")}` : "corrected reply posted");
|
|
1792
1797
|
}
|
|
@@ -2184,7 +2189,7 @@ export class Room extends EventEmitter {
|
|
|
2184
2189
|
return p;
|
|
2185
2190
|
return undefined;
|
|
2186
2191
|
}
|
|
2187
|
-
postSystem(text, audience, wakes) {
|
|
2192
|
+
postSystem(text, audience, wakes, details) {
|
|
2188
2193
|
const message = {
|
|
2189
2194
|
id: randomUUID(),
|
|
2190
2195
|
seq: ++this.seq,
|
|
@@ -2200,6 +2205,8 @@ export class Room extends EventEmitter {
|
|
|
2200
2205
|
message.audience = audience;
|
|
2201
2206
|
if (wakes)
|
|
2202
2207
|
message.wakes = true;
|
|
2208
|
+
if (details)
|
|
2209
|
+
message.details = details;
|
|
2203
2210
|
this.commit(message);
|
|
2204
2211
|
}
|
|
2205
2212
|
async setDir(dir) {
|
package/package.json
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"turnTaking": "parallel",
|
|
12
12
|
"agentsWakeEachOther": false,
|
|
13
13
|
"hopLimit": 12,
|
|
14
|
-
"customRules": "Both build, both explain, both review; @ decides who does what. Without @, a task goes to Forge and a question to Lumen.\nThe human decides when work is reviewed: ask the other one to review it. Unasked, the other answers questions and otherwise stays silent; it does not review, comment on or extend work it was not asked about. When asked to review, first say in one line what you will check, then check it by that criterion: a number, a live check, the risky case. The builder reports to the human, never to the other vibemate.\nMeasure before diagnosing; say no with a reason and a cheaper alternative; keep replies short.\nWork only on your own task, never on the other's; do not touch what the other is working on.\nReply only when addressed. On a message to everyone, only the one it concerns answers (a task: Forge; a question: Lumen); the other stays silent. Never add to, correct or comment on the other's reply unless the human asks.\nFollow the conventions of the project you work in (its instructions, notes, tests). Where it keeps decision notes, write the decision there before reporting it here.\nRead every proposal and analyse it; agree or reject only with a real argument. Never concede to be agreeable, never object to seem rigorous.\nReply in the human's language. Code identifiers, commands, file paths, protocol and tool names stay exactly as in the code; established technical terms (kernel, thread, commit, pull request, layout) stay in English."
|
|
14
|
+
"customRules": "Both build, both explain, both review; @ decides who does what. Without @, a task goes to Forge and a question to Lumen.\nThe human decides when work is reviewed: ask the other one to review it. Unasked, the other answers questions and otherwise stays silent; it does not review, comment on or extend work it was not asked about. When asked to review, first say in one line what you will check, then check it by that criterion: a number, a live check, the risky case. The builder reports to the human, never to the other vibemate. A build is reported as: what changed (the files, and the commit if the project uses version control), how to verify it (a command, a script, a screenshot), what was not verified, what is left.\nMeasure before diagnosing; say no with a reason and a cheaper alternative; keep replies short.\nWork only on your own task, never on the other's; do not touch what the other is working on.\nReply only when addressed. On a message to everyone, only the one it concerns answers (a task: Forge; a question: Lumen); the other stays silent. Never add to, correct or comment on the other's reply unless the human asks.\nFollow the conventions of the project you work in (its instructions, notes, tests). Where it keeps decision notes, write the decision there before reporting it here.\nRead every proposal and analyse it; agree or reject only with a real argument. Never concede to be agreeable, never object to seem rigorous.\nReply in the human's language. Code identifiers, commands, file paths, protocol and tool names stay exactly as in the code; established technical terms (kernel, thread, commit, pull request, layout) stay in English."
|
|
15
15
|
},
|
|
16
16
|
"vibemates": [
|
|
17
17
|
{
|
|
18
18
|
"name": "Forge",
|
|
19
19
|
"tagline": "builds by default; explains and reviews when asked",
|
|
20
|
-
"role": "You are Forge, one of two equal vibemates, Forge and Lumen.
|
|
20
|
+
"role": "You are Forge, one of two equal vibemates, Forge and Lumen. You lean to building: a task without an address is yours; read the code before you change it and keep each change small. Everything else is in the room rules."
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "Lumen",
|
|
24
24
|
"tagline": "explains by default; builds and reviews when asked",
|
|
25
|
-
"role": "You are Lumen, one of two equal vibemates, Forge and Lumen.
|
|
25
|
+
"role": "You are Lumen, one of two equal vibemates, Forge and Lumen. You lean to explaining: a question without an address is yours; when you review, it is by a criterion, not an impression. Everything else is in the room rules."
|
|
26
26
|
}
|
|
27
27
|
]
|
|
28
28
|
}
|
package/ui/app.css
CHANGED
|
@@ -264,7 +264,9 @@
|
|
|
264
264
|
.jump-latest { position: absolute; right: 30px; bottom: 96px; z-index: 5; display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px 0 10px; border: 0; border-radius: var(--r-pill); background: var(--card); color: var(--primary); font: inherit; font-size: 12px; font-weight: 800; box-shadow: var(--shadow-pop); cursor: pointer; animation: rise var(--t-base) var(--ease-out); }
|
|
265
265
|
.jump-latest:hover { background: var(--soft); }
|
|
266
266
|
.done-notes { position: absolute; left: 30px; bottom: 96px; z-index: 5; display: flex; flex-direction: column-reverse; gap: 6px; }
|
|
267
|
-
.done-note { display: inline-flex; align-items:
|
|
267
|
+
.done-note { display: inline-flex; align-items: center; gap: 8px; max-width: 230px; padding: 6px 12px 6px 8px; border: 0; border-radius: 14px; background: var(--warm); color: var(--warm-ink); font: inherit; text-align: left; cursor: pointer; box-shadow: var(--shadow-pop); animation: rise var(--t-base) var(--ease-out); overflow: hidden; }
|
|
268
|
+
.done-note:hover { background: #ffeaa8; }
|
|
269
|
+
.done-note .avatar { flex: none; }
|
|
268
270
|
.done-go { display: inline-flex; align-items: center; gap: 8px; min-width: 0; padding: 6px 4px 6px 8px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
269
271
|
.done-go:hover { background: #ffeaa8; }
|
|
270
272
|
.done-go .avatar { flex: none; }
|
|
@@ -466,6 +468,8 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
466
468
|
.composer textarea::placeholder { color: var(--placeholder); font-weight: 600; }
|
|
467
469
|
.send-btn { width: 44px; height: 44px; border-radius: 14px; border: 0; background: var(--grad-primary); color: #fff; display: grid; place-content: center; box-shadow: var(--shadow-primary); flex: none; transition: transform var(--t-fast) var(--ease-out), filter var(--t-fast); }
|
|
468
470
|
.send-btn .i { width: 18px; height: 18px; display: block; }
|
|
471
|
+
.composer-clear { width: 28px; height: 28px; border-radius: 50%; border: 0; background: transparent; color: var(--placeholder); font-size: 20px; line-height: 1; cursor: pointer; flex: none; margin-right: 2px; transition: background var(--t-fast), color var(--t-fast); }
|
|
472
|
+
.composer-clear:hover { background: var(--soft); color: var(--danger); }
|
|
469
473
|
.send-btn:hover { filter: brightness(1.05); transform: translateY(-1px); }
|
|
470
474
|
.send-btn:active { transform: translateY(1px); }
|
|
471
475
|
.chat .mention-menu { left: 24px; bottom: calc(var(--composer-h, 60px) + 18px); }
|
|
@@ -572,3 +576,9 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
572
576
|
.composer.gated { opacity: 0.7; }
|
|
573
577
|
.composer.gated textarea { cursor: not-allowed; }
|
|
574
578
|
.check-list.locked { pointer-events: none; opacity: 0.6; }
|
|
579
|
+
.msg.system.done { justify-content: center; }
|
|
580
|
+
.done-row { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px 6px 8px; border: 0; border-radius: var(--r-pill); background: var(--warm); color: var(--warm-ink); font: inherit; font-size: 12px; font-weight: 800; cursor: pointer; box-shadow: var(--edge); }
|
|
581
|
+
.done-row:hover { background: #ffeaa8; }
|
|
582
|
+
.done-notes { flex-direction: column; }
|
|
583
|
+
.done-note.new { background: var(--card); color: var(--ink-2); }
|
|
584
|
+
.done-note.new:hover { background: var(--soft); }
|
package/ui/app.js
CHANGED
|
@@ -1210,6 +1210,12 @@
|
|
|
1210
1210
|
}
|
|
1211
1211
|
if (m.kind === "system") {
|
|
1212
1212
|
el.className = "msg system";
|
|
1213
|
+
if (m.audience === "human") {
|
|
1214
|
+
el.className = "msg system done";
|
|
1215
|
+
const who = m.details && m.details.agentId ? findById(room, m.details.agentId) : null;
|
|
1216
|
+
el.innerHTML = `<button type="button" class="done-row" data-ref="${esc((m.details && m.details.refId) || "")}" title="${esc(fullTime(m.ts))} · go to the reply">${who ? avatar(who, 20, {}) : ""}<span>${esc(m.text)}</span></button>`;
|
|
1217
|
+
return el;
|
|
1218
|
+
}
|
|
1213
1219
|
if (m.audience === "agents") {
|
|
1214
1220
|
el.className = "msg hidden";
|
|
1215
1221
|
el.innerHTML = `<details class="hidden-turn"><summary>${ic("info")} hub → vibemates · ${esc(m.text.split(":")[0])}</summary><div class="hidden-body"><div class="hidden-label">What the vibemates were told</div><div class="hidden-text">${esc(m.text)}</div></div></details>`;
|
|
@@ -1291,6 +1297,8 @@
|
|
|
1291
1297
|
if (e.key === "Escape" && !els.lightbox.hidden) closeLightbox();
|
|
1292
1298
|
});
|
|
1293
1299
|
els.messages.addEventListener("click", (e) => {
|
|
1300
|
+
const done = e.target.closest(".done-row");
|
|
1301
|
+
if (done) return void jumpToMessage(els.messages.querySelector(`.msg[data-id="${done.dataset.ref}"]`));
|
|
1294
1302
|
const shot = e.target.closest(".shot");
|
|
1295
1303
|
if (shot) return void openLightbox(shot.dataset.src, shot.title);
|
|
1296
1304
|
const ref = e.target.closest(".img-ref");
|
|
@@ -1528,6 +1536,7 @@
|
|
|
1528
1536
|
if (empty) empty.remove();
|
|
1529
1537
|
els.messages.appendChild(messageElement(room, m));
|
|
1530
1538
|
if (m.from === "human") refreshSeen(room);
|
|
1539
|
+
else if (!stick && m.kind === "chat") noteNew(room, m);
|
|
1531
1540
|
}
|
|
1532
1541
|
if (stick) scrollToBottom();
|
|
1533
1542
|
if (m.streaming) updateWorkingNow();
|
|
@@ -3175,7 +3184,20 @@
|
|
|
3175
3184
|
let shotSeq = 0;
|
|
3176
3185
|
const shotMarker = (n) => `[img ${n}]`;
|
|
3177
3186
|
|
|
3187
|
+
const composerClear = $("#composer-clear");
|
|
3188
|
+
function updateComposerClear() {
|
|
3189
|
+
composerClear.hidden = !(els.input.value.trim() || pendingShots.length);
|
|
3190
|
+
}
|
|
3191
|
+
composerClear.addEventListener("click", () => {
|
|
3192
|
+
els.input.value = "";
|
|
3193
|
+
clearShots();
|
|
3194
|
+
autosize();
|
|
3195
|
+
updateComposerClear();
|
|
3196
|
+
els.input.focus();
|
|
3197
|
+
});
|
|
3198
|
+
els.input.addEventListener("input", updateComposerClear);
|
|
3178
3199
|
function renderShotsTray() {
|
|
3200
|
+
updateComposerClear();
|
|
3179
3201
|
els.shotsTray.hidden = !pendingShots.length;
|
|
3180
3202
|
els.shotsTray.innerHTML = pendingShots
|
|
3181
3203
|
.map((shot, i) => `<span class="shot-chip"><img src="${esc(shot.data)}" alt=""><span class="shot-n">${shot.n}</span><button type="button" class="shot-drop" data-i="${i}" title="Remove ${esc(shot.name)}">×</button></span>`)
|
|
@@ -3652,6 +3674,8 @@
|
|
|
3652
3674
|
els.jumpLatest.hidden = stuck;
|
|
3653
3675
|
updateTimelineView();
|
|
3654
3676
|
updateWorkingNow();
|
|
3677
|
+
if (stuck) clearNotes();
|
|
3678
|
+
else pruneDoneNotes();
|
|
3655
3679
|
});
|
|
3656
3680
|
els.jumpLatest.addEventListener("click", scrollToBottom);
|
|
3657
3681
|
document.addEventListener("mousedown", (e) => {
|
|
@@ -3860,6 +3884,7 @@
|
|
|
3860
3884
|
return (t ? t.textContent : el.textContent).trim().replace(/\s+/g, " ").slice(0, 240);
|
|
3861
3885
|
}
|
|
3862
3886
|
const doneNotes = [];
|
|
3887
|
+
const NOTE_TTL_MS = 4000;
|
|
3863
3888
|
function bubbleInView(id) {
|
|
3864
3889
|
const head = els.messages.querySelector(`.msg[data-id="${id}"] .head`);
|
|
3865
3890
|
if (!head) return false;
|
|
@@ -3867,14 +3892,43 @@
|
|
|
3867
3892
|
const r = head.getBoundingClientRect();
|
|
3868
3893
|
return r.bottom > box.top && r.top < box.bottom;
|
|
3869
3894
|
}
|
|
3895
|
+
function pushNote(note) {
|
|
3896
|
+
const i = doneNotes.findIndex((n) => n.id === note.id);
|
|
3897
|
+
if (i >= 0) {
|
|
3898
|
+
clearTimeout(doneNotes[i].timer);
|
|
3899
|
+
doneNotes.splice(i, 1);
|
|
3900
|
+
}
|
|
3901
|
+
note.timer = setTimeout(() => dropNote(note.id), NOTE_TTL_MS);
|
|
3902
|
+
doneNotes.push(note);
|
|
3903
|
+
while (doneNotes.length > 4) dropNote(doneNotes[0].id);
|
|
3904
|
+
renderDoneNotes();
|
|
3905
|
+
}
|
|
3906
|
+
function dropNote(id) {
|
|
3907
|
+
const i = doneNotes.findIndex((n) => n.id === id);
|
|
3908
|
+
if (i < 0) return;
|
|
3909
|
+
clearTimeout(doneNotes[i].timer);
|
|
3910
|
+
doneNotes.splice(i, 1);
|
|
3911
|
+
renderDoneNotes();
|
|
3912
|
+
}
|
|
3913
|
+
function clearNotes() {
|
|
3914
|
+
for (const n of doneNotes) clearTimeout(n.timer);
|
|
3915
|
+
doneNotes.length = 0;
|
|
3916
|
+
renderDoneNotes();
|
|
3917
|
+
}
|
|
3870
3918
|
function noteFinished(room, m) {
|
|
3871
3919
|
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
3872
3920
|
requestAnimationFrame(() => {
|
|
3873
|
-
if (bubbleInView(m.id)
|
|
3921
|
+
if (bubbleInView(m.id)) return;
|
|
3874
3922
|
const p = findById(room, m.from) || { name: m.fromName, color: "#9ca3af", kind: "agent" };
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3923
|
+
pushNote({ id: m.id, kind: "done", p, text: `${p.name} finished`, sub: `started ${time(m.ts)}${m.durationMs ? ` · ${spanText(m.durationMs)}` : ""}` });
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
function noteNew(room, m) {
|
|
3927
|
+
if (room.id !== state.currentRoomId || state.view !== "room") return;
|
|
3928
|
+
requestAnimationFrame(() => {
|
|
3929
|
+
if (bubbleInView(m.id)) return;
|
|
3930
|
+
const p = findById(room, m.from) || { name: m.fromName, color: "#9ca3af", kind: "agent" };
|
|
3931
|
+
pushNote({ id: m.id, kind: "new", p, text: `${p.name} wrote below`, sub: time(m.ts) });
|
|
3878
3932
|
});
|
|
3879
3933
|
}
|
|
3880
3934
|
function spanText(ms) {
|
|
@@ -3885,19 +3939,18 @@
|
|
|
3885
3939
|
function renderDoneNotes() {
|
|
3886
3940
|
els.doneNotes.hidden = doneNotes.length === 0;
|
|
3887
3941
|
els.doneNotes.innerHTML = doneNotes
|
|
3888
|
-
.map((n) => `<
|
|
3942
|
+
.map((n) => `<button type="button" class="done-note ${n.kind}" data-id="${esc(n.id)}" title="Go to the message">${avatar(n.p, 18, {})}<span class="done-text"><b>${esc(n.text)}</b><small>${esc(n.sub)}</small></span></button>`)
|
|
3889
3943
|
.join("");
|
|
3890
3944
|
}
|
|
3945
|
+
function pruneDoneNotes() {
|
|
3946
|
+
for (const n of [...doneNotes]) if (bubbleInView(n.id)) dropNote(n.id);
|
|
3947
|
+
}
|
|
3891
3948
|
els.doneNotes.addEventListener("click", (e) => {
|
|
3892
3949
|
const note = e.target.closest(".done-note");
|
|
3893
3950
|
if (!note) return;
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
renderDoneNotes();
|
|
3898
|
-
return;
|
|
3899
|
-
}
|
|
3900
|
-
jumpToMessage(els.messages.querySelector(`.msg[data-id="${note.dataset.id}"]`));
|
|
3951
|
+
const id = note.dataset.id;
|
|
3952
|
+
dropNote(id);
|
|
3953
|
+
jumpToMessage(els.messages.querySelector(`.msg[data-id="${id}"]`));
|
|
3901
3954
|
});
|
|
3902
3955
|
function jumpToMessage(el) {
|
|
3903
3956
|
if (!el) return;
|
package/ui/index.html
CHANGED
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
<div class="shots-tray" id="shots-tray" hidden></div>
|
|
104
104
|
<textarea id="input" rows="1" placeholder="Message the room… @Name or /skill" title="Enter sends, Shift+Enter is a new line; paste or drop an image"></textarea>
|
|
105
105
|
</div>
|
|
106
|
+
<button type="button" id="composer-clear" class="composer-clear" title="Clear the message" hidden>×</button>
|
|
106
107
|
<button type="submit" class="send-btn" title="Send"><span data-icon="send"></span></button>
|
|
107
108
|
</form>
|
|
108
109
|
</section>
|