viberoom 0.5.2 → 0.5.3
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 +8 -2
- package/README.md +1 -1
- package/dist/hub.js +46 -2
- package/dist/room.js +43 -3
- package/dist/server.js +18 -1
- package/dist/templates.js +27 -3
- package/package.json +1 -1
- package/templates/wren-and-quinn/template.json +28 -0
- package/ui/app.css +52 -18
- package/ui/app.js +223 -14
- package/ui/fonts/fira-code-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/fira-code-cyrillic.woff2 +0 -0
- package/ui/fonts/fira-code-latin-ext.woff2 +0 -0
- package/ui/fonts/fira-code-latin.woff2 +0 -0
- package/ui/fonts/fira-code.css +5 -0
- package/ui/fonts/inter-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/inter-cyrillic.woff2 +0 -0
- package/ui/fonts/inter-latin-ext.woff2 +0 -0
- package/ui/fonts/inter-latin.woff2 +0 -0
- package/ui/fonts/inter.css +5 -0
- package/ui/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/ui/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/ui/fonts/jetbrains-mono.css +5 -0
- package/ui/fonts/noto-sans-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/noto-sans-cyrillic.woff2 +0 -0
- package/ui/fonts/noto-sans-latin-ext.woff2 +0 -0
- package/ui/fonts/noto-sans-latin.woff2 +0 -0
- package/ui/fonts/noto-sans.css +5 -0
- package/ui/fonts/source-code-pro-cyrillic-ext.woff2 +0 -0
- package/ui/fonts/source-code-pro-cyrillic.woff2 +0 -0
- package/ui/fonts/source-code-pro-latin-ext.woff2 +0 -0
- package/ui/fonts/source-code-pro-latin.woff2 +0 -0
- package/ui/fonts/source-code-pro.css +5 -0
- package/ui/index.html +11 -0
- package/ui/theme.css +44 -18
- package/templates/forge-and-lumen/template.json +0 -28
package/NOTICE
CHANGED
|
@@ -3,8 +3,14 @@ Copyright (c) 2026 Todor Rusev
|
|
|
3
3
|
|
|
4
4
|
This product bundles the following third-party material:
|
|
5
5
|
|
|
6
|
-
-
|
|
7
|
-
|
|
6
|
+
- Fonts (ui/fonts/), each licensed under the SIL Open Font License, Version 1.1
|
|
7
|
+
(ui/fonts/OFL.txt), fetched from Google Fonts by scripts/fetch-fonts.mjs:
|
|
8
|
+
- Nunito: Copyright 2014 The Nunito Project Authors.
|
|
9
|
+
- Inter: Copyright 2016 The Inter Project Authors.
|
|
10
|
+
- Noto Sans: Copyright 2022 The Noto Project Authors.
|
|
11
|
+
- JetBrains Mono: Copyright 2020 The JetBrains Mono Project Authors.
|
|
12
|
+
- Fira Code: Copyright 2014-2020 The Fira Code Project Authors.
|
|
13
|
+
- Source Code Pro: Copyright 2010-2019 Adobe (http://www.adobe.com/).
|
|
8
14
|
|
|
9
15
|
- Vendor icons (assets/vendors/): the logos of Claude, Codex, Gemini, Cursor,
|
|
10
16
|
OpenCode and GitHub Copilot, as published in the Agent Client Protocol
|
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="A
|
|
17
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A Wren & Quinn room: Wren explains a git command from a screenshot, Quinn adds the part that changes whose problem it is">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
package/dist/hub.js
CHANGED
|
@@ -11,6 +11,9 @@ import { DEFAULT_ROOM_SETTINGS } from "./persona.js";
|
|
|
11
11
|
import { Room } from "./room.js";
|
|
12
12
|
import { SkillLibrary } from "./skills.js";
|
|
13
13
|
import { TemplateLibrary } from "./templates.js";
|
|
14
|
+
export const TEXT_FONTS = ["nunito", "inter", "noto-sans", "arial", "system"];
|
|
15
|
+
export const MONO_FONTS = ["jetbrains-mono", "fira-code", "source-code-pro", "system"];
|
|
16
|
+
export const DEFAULT_APPEARANCE = { chatFontSize: 14.5, font: "nunito", mono: "jetbrains-mono" };
|
|
14
17
|
export const DIAGRAM_PRESETS = ["pop", "lavender", "mint", "sunset", "slate"];
|
|
15
18
|
const ROOM_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
16
19
|
const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules"];
|
|
@@ -109,6 +112,7 @@ export class Hub extends EventEmitter {
|
|
|
109
112
|
agentSkillsNeedApproval: false,
|
|
110
113
|
diagrams: { preset: "pop", primary: null },
|
|
111
114
|
editor: { ...DEFAULT_EDITOR_SETTINGS },
|
|
115
|
+
appearance: { ...DEFAULT_APPEARANCE },
|
|
112
116
|
roomDefaults: {},
|
|
113
117
|
vendorPresets: {},
|
|
114
118
|
};
|
|
@@ -186,6 +190,20 @@ export class Hub extends EventEmitter {
|
|
|
186
190
|
throw new Error("editor.command must mention {file} (and usually {line})");
|
|
187
191
|
next.editor = { mode, command };
|
|
188
192
|
}
|
|
193
|
+
if (patch.appearance !== undefined && typeof patch.appearance === "object" && patch.appearance) {
|
|
194
|
+
const a = patch.appearance;
|
|
195
|
+
const chatFontSize = Number(a.chatFontSize ?? next.appearance?.chatFontSize ?? DEFAULT_APPEARANCE.chatFontSize);
|
|
196
|
+
if (!Number.isFinite(chatFontSize) || chatFontSize < 12 || chatFontSize > 24)
|
|
197
|
+
throw new Error("appearance.chatFontSize must be between 12 and 24");
|
|
198
|
+
const font = String(a.font ?? next.appearance?.font ?? DEFAULT_APPEARANCE.font);
|
|
199
|
+
if (!TEXT_FONTS.includes(font))
|
|
200
|
+
throw new Error(`appearance.font must be one of ${TEXT_FONTS.join(", ")}`);
|
|
201
|
+
const mono = String(a.mono ?? next.appearance?.mono ?? DEFAULT_APPEARANCE.mono);
|
|
202
|
+
if (!MONO_FONTS.includes(mono))
|
|
203
|
+
throw new Error(`appearance.mono must be one of ${MONO_FONTS.join(", ")}`);
|
|
204
|
+
next.appearance = { chatFontSize: Math.round(chatFontSize * 2) / 2, font, mono };
|
|
205
|
+
}
|
|
206
|
+
next.appearance = { ...DEFAULT_APPEARANCE, ...(next.appearance ?? {}) };
|
|
189
207
|
if (!next.editor)
|
|
190
208
|
next.editor = { ...DEFAULT_EDITOR_SETTINGS };
|
|
191
209
|
if (patch.roomDefaults !== undefined && typeof patch.roomDefaults === "object" && patch.roomDefaults) {
|
|
@@ -311,9 +329,16 @@ export class Hub extends EventEmitter {
|
|
|
311
329
|
const template = this.templates.get(input.templateId);
|
|
312
330
|
if (!template)
|
|
313
331
|
throw new Error(`no such template: ${input.templateId}`);
|
|
314
|
-
const { room, notices } = this.createRoom({ name: input.name, dir: input.dir, settings: template.settings });
|
|
332
|
+
const { room, notices } = this.createRoom({ name: input.name, dir: input.dir || template.dir || null, settings: template.settings });
|
|
333
|
+
const installed = new Set(listRecipes().filter((r) => !r.unavailableReason).map((r) => r.id));
|
|
315
334
|
for (const [i, tv] of template.vibemates.entries()) {
|
|
316
|
-
const choice = input.vibemates[i] ?? { name: tv.name, agentType: "" };
|
|
335
|
+
const choice = { ...(input.vibemates[i] ?? { name: tv.name, agentType: "" }) };
|
|
336
|
+
if (!choice.agentType && tv.agentType) {
|
|
337
|
+
if (installed.has(tv.agentType))
|
|
338
|
+
choice.agentType = tv.agentType;
|
|
339
|
+
else
|
|
340
|
+
notices.push(`${choice.name || tv.name}: the template runs it on ${tv.agentType}, which is not installed here; pick another agent in the roster.`);
|
|
341
|
+
}
|
|
317
342
|
const skills = (tv.skills ?? []).filter((name) => {
|
|
318
343
|
const ok = !!this.skills.get(name);
|
|
319
344
|
if (!ok)
|
|
@@ -337,6 +362,7 @@ export class Hub extends EventEmitter {
|
|
|
337
362
|
role: tv.role,
|
|
338
363
|
avatar: tv.avatar,
|
|
339
364
|
skills,
|
|
365
|
+
replyDelay: tv.replyDelay,
|
|
340
366
|
model: choice.model ?? tv.model,
|
|
341
367
|
effort: choice.effort ?? tv.effort,
|
|
342
368
|
mode: choice.mode ?? tv.mode,
|
|
@@ -349,6 +375,24 @@ export class Hub extends EventEmitter {
|
|
|
349
375
|
this.saveRooms();
|
|
350
376
|
return { room, notices };
|
|
351
377
|
}
|
|
378
|
+
saveRoomAsTemplate(roomId, input) {
|
|
379
|
+
const name = input.name.trim();
|
|
380
|
+
if (!name)
|
|
381
|
+
throw new Error("the template needs a name");
|
|
382
|
+
const room = this.getRoom(roomId);
|
|
383
|
+
const base = room.templateOf();
|
|
384
|
+
const edited = input.template ?? {};
|
|
385
|
+
const saved = this.templates.save({
|
|
386
|
+
name,
|
|
387
|
+
description: input.description.trim(),
|
|
388
|
+
emoji: (input.emoji ?? room.settings.emoji) || undefined,
|
|
389
|
+
dir: edited.dir?.trim() || base.dir,
|
|
390
|
+
settings: { ...base.settings, ...(edited.settings ?? {}) },
|
|
391
|
+
vibemates: edited.vibemates ?? base.vibemates,
|
|
392
|
+
});
|
|
393
|
+
this.emit("event", { type: "templates" });
|
|
394
|
+
return saved;
|
|
395
|
+
}
|
|
352
396
|
getRoom(id) {
|
|
353
397
|
const room = this.rooms.get(id);
|
|
354
398
|
if (!room)
|
package/dist/room.js
CHANGED
|
@@ -133,6 +133,40 @@ export class Room extends EventEmitter {
|
|
|
133
133
|
this.colorIndex++;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
templateOf() {
|
|
137
|
+
const { name: _n, humanName: _h, ...rest } = this.settings;
|
|
138
|
+
const settings = { ...rest, customRules: this.renderRuleReferences(this.settings.customRules) };
|
|
139
|
+
const vibemates = [];
|
|
140
|
+
for (const p of this.participants.values()) {
|
|
141
|
+
if (p.kind !== "agent" || p.status === "left")
|
|
142
|
+
continue;
|
|
143
|
+
const v = { name: p.name };
|
|
144
|
+
if (p.tagline)
|
|
145
|
+
v.tagline = p.tagline;
|
|
146
|
+
if (p.role)
|
|
147
|
+
v.role = p.role;
|
|
148
|
+
if (p.avatar)
|
|
149
|
+
v.avatar = p.avatar;
|
|
150
|
+
if (p.skills?.length)
|
|
151
|
+
v.skills = [...p.skills];
|
|
152
|
+
if (p.replyDelay !== undefined)
|
|
153
|
+
v.replyDelay = p.replyDelay;
|
|
154
|
+
if (p.agentType) {
|
|
155
|
+
v.agentType = p.agentType;
|
|
156
|
+
const model = p.launch?.model ?? p.model;
|
|
157
|
+
const effort = p.launch?.effort ?? p.effort;
|
|
158
|
+
const mode = p.launch?.mode ?? p.mode;
|
|
159
|
+
if (model)
|
|
160
|
+
v.model = model;
|
|
161
|
+
if (effort)
|
|
162
|
+
v.effort = effort;
|
|
163
|
+
if (mode)
|
|
164
|
+
v.mode = mode;
|
|
165
|
+
}
|
|
166
|
+
vibemates.push(v);
|
|
167
|
+
}
|
|
168
|
+
return { dir: this.dir, settings, vibemates };
|
|
169
|
+
}
|
|
136
170
|
toStored() {
|
|
137
171
|
const { name: _n, humanName: _h, ...settings } = this.settings;
|
|
138
172
|
return {
|
|
@@ -931,6 +965,7 @@ export class Room extends EventEmitter {
|
|
|
931
965
|
turnActive: false,
|
|
932
966
|
pendingTurn: false,
|
|
933
967
|
turn: null,
|
|
968
|
+
strayMessageId: null,
|
|
934
969
|
turnsSinceBrief: 0,
|
|
935
970
|
usedAtBrief: 0,
|
|
936
971
|
briefSentThisTurn: false,
|
|
@@ -1886,11 +1921,16 @@ export class Room extends EventEmitter {
|
|
|
1886
1921
|
const turn = runtime.turn;
|
|
1887
1922
|
switch (update.sessionUpdate) {
|
|
1888
1923
|
case "agent_message_chunk": {
|
|
1889
|
-
if (!turn)
|
|
1890
|
-
return;
|
|
1891
1924
|
const u = update;
|
|
1892
|
-
let text = contentText(u.content);
|
|
1893
1925
|
const messageId = u.messageId ?? null;
|
|
1926
|
+
if (!turn) {
|
|
1927
|
+
if (messageId)
|
|
1928
|
+
runtime.strayMessageId = messageId;
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (messageId && messageId === runtime.strayMessageId)
|
|
1932
|
+
return;
|
|
1933
|
+
let text = contentText(u.content);
|
|
1894
1934
|
if (messageId && !turn.sawMessageId && turn.message.text) {
|
|
1895
1935
|
const notices = (turn.message.notices ??= []);
|
|
1896
1936
|
notices.push(turn.message.text.trim());
|
package/dist/server.js
CHANGED
|
@@ -369,7 +369,7 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
369
369
|
sendJson(res, 200, { ok: true, room: room.snapshot(), notices });
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
|
-
const roomAction = path.match(/^\/api\/rooms\/([^/]+)\/(send|typing|invite|settings|focus|rename|dir|delete|open)$/);
|
|
372
|
+
const roomAction = path.match(/^\/api\/rooms\/([^/]+)\/(send|typing|invite|settings|focus|rename|dir|delete|open|template-preview|save-template)$/);
|
|
373
373
|
if (roomAction) {
|
|
374
374
|
const room = hub.getRoom(decodeURIComponent(roomAction[1]));
|
|
375
375
|
const action = roomAction[2];
|
|
@@ -392,6 +392,23 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
392
392
|
const message = room.postHumanMessage(text, imageList(body.images));
|
|
393
393
|
sendJson(res, 200, { ok: true, id: message.id });
|
|
394
394
|
}
|
|
395
|
+
else if (action === "template-preview") {
|
|
396
|
+
sendJson(res, 200, { template: { name: room.name, emoji: room.settings.emoji || "", ...room.templateOf() } });
|
|
397
|
+
}
|
|
398
|
+
else if (action === "save-template") {
|
|
399
|
+
const edited = body.template && typeof body.template === "object" ? body.template : undefined;
|
|
400
|
+
const template = hub.saveRoomAsTemplate(room.id, {
|
|
401
|
+
name: String(body.name ?? ""),
|
|
402
|
+
description: String(body.description ?? ""),
|
|
403
|
+
emoji: body.emoji === undefined ? undefined : String(body.emoji),
|
|
404
|
+
template: edited && {
|
|
405
|
+
dir: optionalString(edited.dir) ?? undefined,
|
|
406
|
+
settings: edited.settings && typeof edited.settings === "object" ? edited.settings : undefined,
|
|
407
|
+
vibemates: Array.isArray(edited.vibemates) ? edited.vibemates : undefined,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
sendJson(res, 200, { ok: true, template });
|
|
411
|
+
}
|
|
395
412
|
else if (action === "typing") {
|
|
396
413
|
room.humanTyping();
|
|
397
414
|
sendJson(res, 200, { ok: true });
|
package/dist/templates.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
6
7
|
export const SHIPPED_TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
@@ -22,6 +23,8 @@ export function cleanTemplate(raw, id) {
|
|
|
22
23
|
}
|
|
23
24
|
if (Array.isArray(o.skills))
|
|
24
25
|
out.skills = o.skills.map((s) => String(s).trim()).filter(Boolean);
|
|
26
|
+
if (typeof o.replyDelay === "number" && Number.isFinite(o.replyDelay))
|
|
27
|
+
out.replyDelay = o.replyDelay;
|
|
25
28
|
return out;
|
|
26
29
|
});
|
|
27
30
|
const settings = (t.settings && typeof t.settings === "object" ? t.settings : {});
|
|
@@ -32,8 +35,16 @@ export function cleanTemplate(raw, id) {
|
|
|
32
35
|
out.emoji = t.emoji.trim().slice(0, 8);
|
|
33
36
|
if (t.recommended === true)
|
|
34
37
|
out.recommended = true;
|
|
38
|
+
if (typeof t.dir === "string" && t.dir.trim())
|
|
39
|
+
out.dir = t.dir.trim();
|
|
40
|
+
if (typeof t.created === "string" && t.created.trim())
|
|
41
|
+
out.created = t.created.trim();
|
|
35
42
|
return out;
|
|
36
43
|
}
|
|
44
|
+
export function templateId(name) {
|
|
45
|
+
const id = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
46
|
+
return ID_PATTERN.test(id) ? id : "template";
|
|
47
|
+
}
|
|
37
48
|
function readTemplates(dir, log, builtin) {
|
|
38
49
|
const out = [];
|
|
39
50
|
if (!existsSync(dir))
|
|
@@ -54,7 +65,7 @@ function readTemplates(dir, log, builtin) {
|
|
|
54
65
|
log.warn(`template ${entry.name} in ${dir} skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
|
-
return out.sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.name.localeCompare(b.name));
|
|
68
|
+
return out.sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || (b.created ?? "").localeCompare(a.created ?? "") || a.name.localeCompare(b.name));
|
|
58
69
|
}
|
|
59
70
|
export class TemplateLibrary {
|
|
60
71
|
dir;
|
|
@@ -68,9 +79,22 @@ export class TemplateLibrary {
|
|
|
68
79
|
list() {
|
|
69
80
|
const own = readTemplates(this.dir, this.log, false);
|
|
70
81
|
const taken = new Set(own.map((t) => t.id));
|
|
71
|
-
return [...readTemplates(this.shippedDir, this.log, true).filter((t) => !taken.has(t.id))
|
|
82
|
+
return [...own, ...readTemplates(this.shippedDir, this.log, true).filter((t) => !taken.has(t.id))];
|
|
72
83
|
}
|
|
73
84
|
get(id) {
|
|
74
85
|
return this.list().find((t) => t.id === id);
|
|
75
86
|
}
|
|
87
|
+
save(template) {
|
|
88
|
+
const taken = new Set(this.list().map((t) => t.id));
|
|
89
|
+
const base = templateId(template.name);
|
|
90
|
+
let id = base;
|
|
91
|
+
for (let n = 2; taken.has(id); n++)
|
|
92
|
+
id = `${base.slice(0, 40 - String(n).length - 1)}-${n}`;
|
|
93
|
+
const clean = cleanTemplate({ ...template, created: new Date().toISOString() }, id);
|
|
94
|
+
const folder = join(this.dir, id);
|
|
95
|
+
mkdirSync(folder, { recursive: true });
|
|
96
|
+
writeFileAtomic(join(folder, "template.json"), JSON.stringify(clean, null, 2) + "\n");
|
|
97
|
+
this.log.info(`saved template "${clean.name}" (${id})`);
|
|
98
|
+
return clean;
|
|
99
|
+
}
|
|
76
100
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Wren & Quinn",
|
|
3
|
+
"order": 1,
|
|
4
|
+
"emoji": "⚒️",
|
|
5
|
+
"recommended": true,
|
|
6
|
+
"description": "Two equals around one codebase. Wren builds by default, Quinn explains by default; either does either when asked, and either reviews the other's work when you ask for it. The way viberoom itself was built.",
|
|
7
|
+
"settings": {
|
|
8
|
+
"language": {
|
|
9
|
+
"mode": "follow-human"
|
|
10
|
+
},
|
|
11
|
+
"turnTaking": "parallel",
|
|
12
|
+
"agentsWakeEachOther": true,
|
|
13
|
+
"hopLimit": 24,
|
|
14
|
+
"customRules": "Both build, both explain, both review; @ decides who does what. Without @, a task goes to the one who has worked on that part the most, or most recently; if neither has touched it, to Wren. A question without @ goes to Quinn.\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; 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
|
+
},
|
|
16
|
+
"vibemates": [
|
|
17
|
+
{
|
|
18
|
+
"name": "Wren",
|
|
19
|
+
"tagline": "builds by default; explains and reviews when asked",
|
|
20
|
+
"role": "You are Wren, one of two equal vibemates, Wren and Quinn. You lean to building: a task without an address is yours when it touches what you have built, or when neither of you has; before you change anything, read what is already there, the notes, the docs and the code, and keep each change small. Everything else is in the room rules."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "Quinn",
|
|
24
|
+
"tagline": "explains by default; builds and reviews when asked",
|
|
25
|
+
"role": "You are Quinn, one of two equal vibemates, Wren and Quinn. 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
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
package/ui/app.css
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
.rail-room:hover .room-mark { opacity: 1; transform: translateY(-1px); }
|
|
36
36
|
.rail-room.active, .rail-room.active:hover { background: transparent; box-shadow: none; }
|
|
37
37
|
.rail-room.active .room-mark { opacity: 1; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--primary); }
|
|
38
|
-
.rail-room .rail-count { background:
|
|
38
|
+
.rail-room .rail-count { background: var(--unread-grad); min-width: 18px; text-align: center; z-index: 2; }
|
|
39
39
|
.rail-room .rail-count.bump { animation: badge-pop 360ms var(--ease-pop); }
|
|
40
40
|
@keyframes badge-pop { 0% { transform: scale(0.6); } 55% { transform: scale(1.3); } 100% { transform: scale(1); } }
|
|
41
41
|
.shell.rail-open .rail-items { align-items: stretch; padding: 0 14px; }
|
|
@@ -214,9 +214,9 @@
|
|
|
214
214
|
.timeline { position: absolute; right: 6px; top: 78px; bottom: calc(var(--composer-h, 60px) + 36px); width: 18px; z-index: 4; }
|
|
215
215
|
.tl-ticks { position: absolute; inset: 0; }
|
|
216
216
|
.tl-view { position: absolute; left: 2px; right: 2px; border-radius: 6px; background: rgba(91, 91, 240, 0.07); pointer-events: none; transition: top 80ms linear, height 80ms linear; }
|
|
217
|
-
.tl-tick { position: absolute; left: 3px; width: 12px; height: 5px; border-radius: 3px; background:
|
|
217
|
+
.tl-tick { position: absolute; left: 3px; width: 12px; height: 5px; border-radius: 3px; background: var(--tick-mine); cursor: pointer; transition: background var(--t-fast), transform var(--t-fast); }
|
|
218
218
|
.tl-tick:hover, .tl-tick.active { background: var(--primary); transform: scaleX(1.25); }
|
|
219
|
-
.tl-tick.in-view { background:
|
|
219
|
+
.tl-tick.in-view { background: var(--tick-mine-view); }
|
|
220
220
|
.tl-tick.pinned { background: var(--primary); }
|
|
221
221
|
.tl-row.current.pinned { background: var(--lav-2); }
|
|
222
222
|
.tl-row.pinned { opacity: 1; color: var(--ink); }
|
|
@@ -224,9 +224,9 @@
|
|
|
224
224
|
.tl-row .tl-pin .i { width: 14px; height: 14px; }
|
|
225
225
|
.timeline.left { left: 6px; right: auto; }
|
|
226
226
|
.timeline.left .tl-view { background: rgba(28, 27, 51, 0.05); }
|
|
227
|
-
.timeline.left .tl-tick { background: color-mix(in srgb, var(--tick,
|
|
228
|
-
.timeline.left .tl-tick.in-view { background: color-mix(in srgb, var(--tick,
|
|
229
|
-
.timeline.left .tl-tick.pinned, .timeline.left .tl-tick:hover, .timeline.left .tl-tick.active { background: var(--tick,
|
|
227
|
+
.timeline.left .tl-tick { background: color-mix(in srgb, var(--tick, var(--tick-fallback)) 35%, #fff); }
|
|
228
|
+
.timeline.left .tl-tick.in-view { background: color-mix(in srgb, var(--tick, var(--tick-fallback)) 60%, #fff); }
|
|
229
|
+
.timeline.left .tl-tick.pinned, .timeline.left .tl-tick:hover, .timeline.left .tl-tick.active { background: var(--tick, var(--tick-fallback)); }
|
|
230
230
|
.timeline.left .tl-pop { left: 26px; right: auto; }
|
|
231
231
|
.tl-pop::before { content: ""; position: absolute; top: -6px; bottom: -6px; right: -6px; width: 6px; }
|
|
232
232
|
.timeline.left .tl-pop::before { right: auto; left: -6px; }
|
|
@@ -265,25 +265,25 @@
|
|
|
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
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:
|
|
268
|
+
.done-note:hover { background: var(--note-hover); }
|
|
269
269
|
.done-note .avatar { flex: none; }
|
|
270
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; }
|
|
271
|
-
.done-go:hover { background:
|
|
271
|
+
.done-go:hover { background: var(--note-hover); }
|
|
272
272
|
.done-go .avatar { flex: none; }
|
|
273
273
|
.done-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
|
|
274
274
|
.done-text b { font-size: 12px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
275
275
|
.done-text small { font-size: 11px; font-weight: 600; opacity: 0.8; white-space: nowrap; }
|
|
276
276
|
.done-x { border: 0; background: transparent; color: inherit; opacity: 0.6; font-size: 15px; line-height: 1; padding: 0 9px; cursor: pointer; }
|
|
277
|
-
.done-x:hover { opacity: 1; background:
|
|
277
|
+
.done-x:hover { opacity: 1; background: var(--note-hover); }
|
|
278
278
|
.jump-latest .i { width: 16px; height: 16px; }
|
|
279
279
|
.bubble { max-width: 100%; background: #fff; border-radius: 6px 18px 18px 18px; padding: 12px 16px; min-width: 0; font-size: 14.5px; font-weight: 600; line-height: 1.55; color: var(--ink-2); box-shadow: 0 2px 0 rgba(28, 27, 51, 0.06), 0 1px 3px rgba(28, 27, 51, 0.04); }
|
|
280
280
|
.msg.mine .bubble { background: var(--grad-primary); color: #fff; border-radius: 18px 18px 6px 18px; box-shadow: 0 8px 18px -10px rgba(91, 91, 240, 0.7); padding: 11px 16px; }
|
|
281
281
|
.msg.mine .bubble .text { color: #fff; }
|
|
282
|
-
.msg.mine.waiting .bubble { background:
|
|
282
|
+
.msg.mine.waiting .bubble { background: var(--attention-grad); box-shadow: var(--attention-shadow); }
|
|
283
283
|
.bubble .waiting { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; padding-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.35); font-size: 13px; font-weight: 600; color: rgba(255, 255, 255, 0.92); }
|
|
284
284
|
.bubble .waiting .i { width: 14px; height: 14px; vertical-align: -2px; }
|
|
285
285
|
.bubble .waiting-text { flex: 1; min-width: 160px; }
|
|
286
|
-
.bubble .waiting-stop { border: 0; border-radius: 10px; padding: 6px 12px; background: rgba(255, 255, 255, 0.92); color:
|
|
286
|
+
.bubble .waiting-stop { border: 0; border-radius: 10px; padding: 6px 12px; background: rgba(255, 255, 255, 0.92); color: var(--attention-ink); font-weight: 800; font-size: 12.5px; cursor: pointer; }
|
|
287
287
|
.bubble .waiting-stop:hover:not(:disabled) { background: #fff; }
|
|
288
288
|
.bubble .waiting-stop:disabled { opacity: 0.7; cursor: default; }
|
|
289
289
|
.msg.mine .mention { background: rgba(255, 255, 255, 0.22); color: #fff !important; padding: 1px 7px; border-radius: 6px; font-weight: 800; }
|
|
@@ -361,10 +361,17 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
361
361
|
.bubble .text.clamped { max-height: 9.2em; overflow: hidden; -webkit-mask-image: linear-gradient(180deg, #000 70%, transparent); mask-image: linear-gradient(180deg, #000 70%, transparent); }
|
|
362
362
|
.bubble .more { background: none; border: 0; color: var(--primary); font-weight: 800; padding: 2px 0; font-size: 13px; }
|
|
363
363
|
.msg.mine .bubble .more { color: #fff; }
|
|
364
|
-
.bubble .pending {
|
|
364
|
+
.bubble .pending { display: inline-flex; align-items: center; gap: 4px; height: 1.55em; vertical-align: text-bottom; }
|
|
365
|
+
.bubble .pending i { width: 5px; height: 5px; border-radius: 50%; background: var(--muted); opacity: 0.25; animation: think 1.4s ease-in-out infinite; }
|
|
366
|
+
.bubble .pending i:nth-child(2) { animation-delay: 0.2s; }
|
|
367
|
+
.bubble .pending i:nth-child(3) { animation-delay: 0.4s; }
|
|
368
|
+
@keyframes think { 0%, 60%, 100% { opacity: 0.25; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }
|
|
365
369
|
.mention { font-weight: 800; }
|
|
366
|
-
.
|
|
367
|
-
|
|
370
|
+
.working { display: inline-block; width: 19px; height: 15px; color: var(--faint); margin-left: 5px; vertical-align: -3px; overflow: visible; }
|
|
371
|
+
.working .forearm { animation: type 0.14s ease-in-out infinite alternate; transform-box: view-box; transform-origin: 23px 27px; }
|
|
372
|
+
.working .body { animation: bob 0.28s ease-in-out infinite alternate; }
|
|
373
|
+
@keyframes type { from { transform: rotate(-16deg); } to { transform: rotate(4deg); } }
|
|
374
|
+
@keyframes bob { from { transform: translateY(0); } to { transform: translateY(-0.8px); } }
|
|
368
375
|
.thought { margin: 2px 0 6px; font-size: 12px; font-weight: 600; color: var(--muted); }
|
|
369
376
|
.thought summary { cursor: pointer; }
|
|
370
377
|
.thought-text { white-space: pre-wrap; padding: 4px 0 0 8px; border-left: 2px solid var(--lav-2); margin-top: 4px; }
|
|
@@ -403,8 +410,8 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
403
410
|
.head .edit-btn .i, .head .pin-btn .i { width: 14px; height: 14px; }
|
|
404
411
|
.msg:hover .head .edit-btn, .head .edit-btn:focus, .msg:hover .head .pin-btn, .head .pin-btn:focus { opacity: 1; }
|
|
405
412
|
.head .edit-btn:hover { color: var(--primary); }
|
|
406
|
-
.head .pin-btn:hover { color:
|
|
407
|
-
.msg.pinned .head .pin-btn { opacity: 1; color:
|
|
413
|
+
.head .pin-btn:hover { color: var(--attention); }
|
|
414
|
+
.msg.pinned .head .pin-btn { opacity: 1; color: var(--attention); }
|
|
408
415
|
.edit-box { margin: 4px 0 6px; }
|
|
409
416
|
.msg .bubble-col:has(> .bubble > .edit-box:not([hidden])) { width: 100%; }
|
|
410
417
|
.msg .bubble:has(> .edit-box:not([hidden])) { width: min(760px, 100%); }
|
|
@@ -428,7 +435,7 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
428
435
|
.perm-result { color: var(--muted); font-size: 12px; }
|
|
429
436
|
.visibility-divider { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
|
|
430
437
|
.visibility-divider .vd-line { flex: 1; height: 8px; background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='8' viewBox='0 0 16 8'><path d='M0 4 Q4 0 8 4 T16 4' fill='none' stroke='%23f472b6' stroke-width='1.6'/></svg>") repeat-x center / 16px 8px; opacity: 0.7; }
|
|
431
|
-
.visibility-divider .vd-label { color:
|
|
438
|
+
.visibility-divider .vd-label { color: var(--unseen-ink); background: var(--unseen-bg); border-radius: var(--r-pill); padding: 4px 12px; font-size: 11px; font-weight: 800; white-space: nowrap; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
|
|
432
439
|
.msg.hidden { justify-content: center; }
|
|
433
440
|
.hidden-turn { max-width: 720px; width: 100%; background: var(--soft); border-radius: 12px; padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--muted); }
|
|
434
441
|
.hidden-turn summary { cursor: pointer; list-style: none; }
|
|
@@ -562,6 +569,31 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
562
569
|
.tpl-meta { color: var(--muted); font-size: 12px; font-weight: 600; }
|
|
563
570
|
.tpl-item .avatar-stack { margin-top: 2px; }
|
|
564
571
|
.tpl-rec { background: var(--warm); color: var(--warm-ink); font-size: 10px; padding: 1px 7px; }
|
|
572
|
+
.tpl-mine { background: var(--lav); color: var(--primary); font-size: 10px; padding: 1px 7px; }
|
|
573
|
+
.tpl-runs { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
574
|
+
.stp-preview { display: flex; flex-direction: column; gap: 10px; max-height: 48vh; overflow: auto; padding-right: 4px; margin: 4px 0 10px; }
|
|
575
|
+
.stp-section { background: var(--soft); border-radius: 14px; padding: 10px 12px; }
|
|
576
|
+
.section.template { background: var(--lav); margin: 22px 0 14px; padding: 16px 18px 18px; box-sizing: border-box; width: 100%; overflow: hidden; }
|
|
577
|
+
.section.template > h4 { color: var(--primary); }
|
|
578
|
+
.stp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
|
|
579
|
+
.stp-grid .field { margin-bottom: 8px; }
|
|
580
|
+
.stp-grid .field.wide { grid-column: 1 / -1; }
|
|
581
|
+
.stp-grid .switch { margin: 2px 0 8px; }
|
|
582
|
+
.stp-vm { display: grid; grid-template-columns: 36px 1fr; gap: 10px; align-items: start; padding: 10px 0 4px; border-top: 1px solid var(--lav-2); }
|
|
583
|
+
.stp-vm-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0 10px; }
|
|
584
|
+
.stp-vm-fields .field { margin-bottom: 8px; }
|
|
585
|
+
.stp-vm-fields .field.wide { grid-column: 1 / -1; }
|
|
586
|
+
.stp-vm-fields .field textarea { min-height: 54px; }
|
|
587
|
+
.stp-vm-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
588
|
+
.stp-vm-top b { flex: 1; }
|
|
589
|
+
.stp-section h5 { margin: 0 0 6px; font-size: 11px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint); }
|
|
590
|
+
.stp-section .kv { font-size: 12.5px; }
|
|
591
|
+
.stp-section ul { margin: 0; padding-left: 18px; color: var(--ink-2); font-size: 13px; font-weight: 600; }
|
|
592
|
+
.stp-vm { display: grid; grid-template-columns: 36px 1fr; gap: 10px; align-items: start; padding: 8px 0; border-top: 1px solid var(--lav-2); }
|
|
593
|
+
.stp-vm:first-of-type { border-top: 0; padding-top: 2px; }
|
|
594
|
+
.stp-vm-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
|
|
595
|
+
.stp-vm-role { color: var(--muted); font-size: 12.5px; font-weight: 600; line-height: 1.45; margin-top: 3px; }
|
|
596
|
+
.stp-vm .chips { margin-top: 5px; }
|
|
565
597
|
.tpl-check { display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; background: var(--primary); color: #fff; opacity: 0; transition: opacity var(--t-fast); }
|
|
566
598
|
.tpl-check .i { width: 12px; height: 12px; }
|
|
567
599
|
.tpl-item.on .tpl-check { opacity: 1; }
|
|
@@ -591,8 +623,10 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
|
|
|
591
623
|
.check-list.locked { pointer-events: none; opacity: 0.6; }
|
|
592
624
|
.msg.system.done { justify-content: center; }
|
|
593
625
|
.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); }
|
|
594
|
-
.done-row:hover { background:
|
|
626
|
+
.done-row:hover { background: var(--note-hover); }
|
|
595
627
|
.done-notes { flex-direction: column; }
|
|
596
628
|
.done-note.new { background: var(--card); color: var(--ink-2); }
|
|
597
629
|
.done-note.new:hover { background: var(--soft); }
|
|
598
630
|
.meta .seen-by { color: var(--primary); font-weight: 800; margin-left: 6px; }
|
|
631
|
+
.section.template .stp-row { justify-content: center; margin: 6px 0 0; }
|
|
632
|
+
.section.template .field-note { margin: -2px 0 12px; }
|
package/ui/app.js
CHANGED
|
@@ -128,7 +128,39 @@
|
|
|
128
128
|
eraseSubmit: $("#erase-submit"),
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
-
const
|
|
131
|
+
const FONTS = {
|
|
132
|
+
text: {
|
|
133
|
+
nunito: { label: "Nunito (default)", stack: '"Nunito", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
134
|
+
inter: { label: "Inter", stack: '"Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
135
|
+
"noto-sans": { label: "Noto Sans", stack: '"Noto Sans", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif' },
|
|
136
|
+
arial: { label: "Arial / Helvetica (system)", stack: 'Arial, Helvetica, "Liberation Sans", sans-serif' },
|
|
137
|
+
system: { label: "System UI font", stack: 'system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, sans-serif' },
|
|
138
|
+
},
|
|
139
|
+
mono: {
|
|
140
|
+
"jetbrains-mono": { label: "JetBrains Mono (default)", stack: '"JetBrains Mono", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
141
|
+
"fira-code": { label: "Fira Code", stack: '"Fira Code", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
142
|
+
"source-code-pro": { label: "Source Code Pro", stack: '"Source Code Pro", ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", monospace' },
|
|
143
|
+
system: { label: "System monospace", stack: 'ui-monospace, Consolas, Menlo, "DejaVu Sans Mono", "Courier New", monospace' },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const STATUS_LABEL = { unstaffed: "needs a coding agent", starting: "starting…", idle: "ready", queued: "waiting…", thinking: "thinking…", writing: "writing…", error: "error", offline: "offline", left: "left" };
|
|
147
|
+
const FALLBACK_COLOR = "#9ca3af";
|
|
148
|
+
const WORKING_SVG = '<svg class="working" viewBox="0 0 44 35" aria-hidden="true" title="working…">'
|
|
149
|
+
+ '<g class="body">'
|
|
150
|
+
+ '<circle cx="20" cy="6.5" r="5.6" fill="currentColor"/>'
|
|
151
|
+
+ '<path d="M6 35L13.7 16.5a4 4 0 0 1 8 0L14 35z" fill="currentColor"/>'
|
|
152
|
+
+ '</g>'
|
|
153
|
+
+ '<path d="M19 19.5L23 27" fill="none" stroke="currentColor" stroke-width="4.4" stroke-linecap="round"/>'
|
|
154
|
+
+ '<g class="forearm"><path d="M23 27h10.5" fill="none" stroke="currentColor" stroke-width="4.4" stroke-linecap="round"/></g>'
|
|
155
|
+
+ '<rect x="26" y="30.2" width="15.5" height="3" rx="1" fill="currentColor"/>'
|
|
156
|
+
+ '<path d="M35.9 30.2L41.1 14h2.4l-5.1 16.2z" fill="currentColor"/>'
|
|
157
|
+
+ '<rect x="18" y="33.2" width="25" height="1.3" rx=".6" fill="currentColor"/>'
|
|
158
|
+
+ '</svg>';
|
|
159
|
+
|
|
160
|
+
function shownStatus(room, p) {
|
|
161
|
+
if (p.status !== "thinking") return p.status;
|
|
162
|
+
return room.messages.some((m) => m.streaming && m.from === p.id) ? "writing" : "thinking";
|
|
163
|
+
}
|
|
132
164
|
const CHAT_EMOJI = ["😀", "😄", "😂", "🙂", "😉", "😍", "🤔", "😎", "🥳", "😅", "😢", "😡", "👍", "👎", "👋", "🙏", "👏", "💪", "🔥", "✨", "🎉", "❤️", "💜", "✅", "❌", "⚠️", "💡", "🚀", "🐛", "🤖", "🤫", "☕"];
|
|
133
165
|
const ROOM_EMOJI = ["🎭", "🚀", "🧪", "🛠️", "🎨", "📚", "🧠", "💬", "🔬", "🎯", "🐙", "☕", "🌈", "🏗️", "🎮", "🔥", "🧩", "📈", "🗺️", "🎧", "🌱", "🏠", "🛸", "🧭"];
|
|
134
166
|
function emojiGrid(list, current, onPick) {
|
|
@@ -391,7 +423,7 @@
|
|
|
391
423
|
function mermaidThemeVariables(d) {
|
|
392
424
|
d = diagramSettings(d);
|
|
393
425
|
const preset = DIAGRAM_PRESETS[d.preset] || DIAGRAM_PRESETS.pop;
|
|
394
|
-
const vars = Object.assign({}, preset, { fontFamily: "
|
|
426
|
+
const vars = Object.assign({}, preset, { fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font").trim() || "Nunito, sans-serif", fontSize: "13px" });
|
|
395
427
|
delete vars.label;
|
|
396
428
|
delete vars.palette;
|
|
397
429
|
if (preset.palette) preset.palette.forEach((c, i) => (vars[`pie${i + 1}`] = c.fill));
|
|
@@ -1070,6 +1102,7 @@
|
|
|
1070
1102
|
const selected = (state.selection.kind === "participant" && state.selection.id === p.id) || (p.kind === "human" && state.selection.kind === "me" && state.detailsOpen);
|
|
1071
1103
|
const asleep = p.kind === "agent" && (p.status === "offline" || p.status === "left");
|
|
1072
1104
|
const unstaffed = p.kind === "agent" && p.status === "unstaffed";
|
|
1105
|
+
const shown = shownStatus(room, p);
|
|
1073
1106
|
const className = (p.kind === "human" ? "me" : "") + (selected ? " selected" : "") + (asleep ? " offline" : "") + (unstaffed ? " unstaffed" : "");
|
|
1074
1107
|
const sub = p.kind === "human" ? "you, the human" : [p.tagline ? `"${p.tagline}"` : "", p.agentVendor || p.agentLabel, p.model].filter(Boolean).join(" · ");
|
|
1075
1108
|
const warn = p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<div class="p-warn" title="${esc(p.statusDetail)}">${esc(p.statusDetail)}</div>` : "";
|
|
@@ -1077,9 +1110,9 @@
|
|
|
1077
1110
|
? `<span class="badge status-unstaffed" title="Click to summon this vibemate: pick the coding agent that runs it">summon</span>`
|
|
1078
1111
|
: asleep
|
|
1079
1112
|
? `<span class="zzz" title="${esc(STATUS_LABEL[p.status] || p.status)}">zzz</span>`
|
|
1080
|
-
: p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${
|
|
1113
|
+
: p.kind === "agent" && p.status !== "idle" ? `<span class="badge status-${shown}">${p.status === "thinking" ? '<span class="dot"></span>' : ""}${STATUS_LABEL[shown] || shown}</span>` : "";
|
|
1081
1114
|
const avatarHtml = avatar(p.kind === "human" ? meAvatarData() : p, 44, { vendor: true });
|
|
1082
|
-
const statusClass = p.kind === "agent" ? `avatar-status status-${esc(
|
|
1115
|
+
const statusClass = p.kind === "agent" ? `avatar-status status-${esc(shown || "idle")}` : "";
|
|
1083
1116
|
const bodyHtml = `<div class="p-body">
|
|
1084
1117
|
<div class="p-name"><span>${esc(p.name)}</span>${p.muted ? '<span class="badge muted">muted</span>' : ""}${status}</div>
|
|
1085
1118
|
<div class="p-sub">${esc(sub)}</div>
|
|
@@ -1228,7 +1261,7 @@
|
|
|
1228
1261
|
: `<div class="sys${warn ? " warn" : ""}" title="${esc(fullTime(m.ts))}">${esc(m.text)}</div>`;
|
|
1229
1262
|
return el;
|
|
1230
1263
|
}
|
|
1231
|
-
const p = findById(room, m.from) || { name: m.fromName, color:
|
|
1264
|
+
const p = findById(room, m.from) || { name: m.fromName, color: FALLBACK_COLOR, kind: m.from === "human" ? "human" : "agent" };
|
|
1232
1265
|
const mine = m.from === "human";
|
|
1233
1266
|
el.className = "msg " + (mine ? "mine" : "agent");
|
|
1234
1267
|
el.innerHTML = `
|
|
@@ -1377,9 +1410,9 @@
|
|
|
1377
1410
|
const more = el.querySelector(".more");
|
|
1378
1411
|
const long = !m.streaming && m.text.length > CLAMP_CHARS;
|
|
1379
1412
|
const expanded = state.expanded.has(m.id);
|
|
1380
|
-
text.innerHTML = renderText(room, m.text, m.images) + (m.streaming ?
|
|
1413
|
+
text.innerHTML = renderText(room, m.text, m.images) + (m.streaming ? WORKING_SVG : "");
|
|
1381
1414
|
if (!m.streaming) renderDiagrams(text);
|
|
1382
|
-
if (m.streaming && !m.text) text.innerHTML = '<span class="pending"
|
|
1415
|
+
if (m.streaming && !m.text) text.innerHTML = '<span class="pending" title="thinking…"><i></i><i></i><i></i></span>';
|
|
1383
1416
|
text.classList.toggle("clamped", long && !expanded);
|
|
1384
1417
|
more.hidden = !long;
|
|
1385
1418
|
more.textContent = expanded ? "Show less" : "Show more";
|
|
@@ -1434,8 +1467,9 @@
|
|
|
1434
1467
|
if (!present.length) return "";
|
|
1435
1468
|
const seen = present.filter((p) => p.lastSeenSeq != null && p.lastSeenSeq >= m.seq);
|
|
1436
1469
|
if (seen.length === present.length) return "";
|
|
1437
|
-
|
|
1438
|
-
return `<span class="ticks"
|
|
1470
|
+
const you = m.from !== "human";
|
|
1471
|
+
if (!seen.length) return `<span class="ticks">✓</span> sent${you ? " · seen by you" : ""}`;
|
|
1472
|
+
return `<span class="ticks">✓✓</span> seen by ${esc([...(you ? ["you"] : []), ...seen.map((p) => p.name)].join(", "))}`;
|
|
1439
1473
|
}
|
|
1440
1474
|
function fillSeen(el, room, m) {
|
|
1441
1475
|
if (m.kind !== "chat" || m.streaming) return;
|
|
@@ -1561,6 +1595,7 @@
|
|
|
1561
1595
|
if (empty) empty.remove();
|
|
1562
1596
|
els.messages.appendChild(messageElement(room, m));
|
|
1563
1597
|
if (m.from === "human") refreshSeen(room);
|
|
1598
|
+
if (m.streaming && m.from !== "human") renderSideRoom();
|
|
1564
1599
|
else if (!stick && m.kind === "chat") noteNew(room, m);
|
|
1565
1600
|
}
|
|
1566
1601
|
if (stick) scrollToBottom();
|
|
@@ -1997,7 +2032,7 @@
|
|
|
1997
2032
|
${field("Emoji", `<div id="rp-emoji-picker"></div><input type="text" id="rp-emoji" maxlength="8" value="${esc(rs.emoji || "")}" placeholder="custom emoji (optional)">`, "A face for the room, next to its name.")}
|
|
1998
2033
|
${field("Topic", `<input type="text" id="rp-topic" maxlength="2000" value="${esc(rs.topic || "")}" placeholder="what this room is about (optional)">`)}
|
|
1999
2034
|
${field("Folder", `<span class="dir-row"><input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false"><button type="button" class="btn ghost browse-btn" id="rp-dir-browse" title="Choose a folder">${ic("folder")}Browse</button></span>`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
2000
|
-
<
|
|
2035
|
+
<div class="field mention-host"><span class="label">Room rules${geekTip("References follow renames and note when a participant has left. Rules go into every vibemate's brief as instructions, not as routing.")}</span><div id="rp-rules" class="rules-editor" contenteditable="true" spellcheck="true" data-placeholder="e.g. Everyone listens to @Pesho, he is the manager. Keep answers under 3 sentences."></div><span class="hint">One rule per line; type @ to reference a participant.</span><div class="mention-menu inline" id="rp-rules-menu" hidden></div></div>
|
|
2001
2036
|
${field("Language", `<input type="text" id="rp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
2002
2037
|
</div>
|
|
2003
2038
|
<div class="section">
|
|
@@ -2007,6 +2042,11 @@
|
|
|
2007
2042
|
<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>
|
|
2008
2043
|
<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>
|
|
2009
2044
|
</div>
|
|
2045
|
+
<div class="section template">
|
|
2046
|
+
${sectionTitle("rooms", "Turn this room into a template")}
|
|
2047
|
+
<p class="field-note">Its settings, rules, folder and vibemates (with the coding agent each runs on) become one of your templates, listed first under "Start from a template". You see everything it will contain, and can change any of it, before you create it.</p>
|
|
2048
|
+
<div class="row-btns start stp-row"><button type="button" class="btn sm primary" id="rp-template">${ic("rooms")}Preview and create template</button></div>
|
|
2049
|
+
</div>
|
|
2010
2050
|
${geek(
|
|
2011
2051
|
"rp-geek",
|
|
2012
2052
|
`<div class="section">
|
|
@@ -2034,6 +2074,7 @@
|
|
|
2034
2074
|
<label class="switch"><span class="label">Repeat core rules in every header</span><input type="checkbox" id="rp-header-rules" ${rs.headerRules ? "checked" : ""}></label>
|
|
2035
2075
|
<label class="switch"><span class="label">Show vendor and model to other vibemates</span><input type="checkbox" id="rp-vendor" ${rs.showVendorInRoster ? "checked" : ""}></label>
|
|
2036
2076
|
${field("Replay last N chat messages after a reconnect", `<input type="number" id="rp-replay" min="0" max="200" value="${rs.replayAfterRestart}">`)}
|
|
2077
|
+
${field("Missed messages a vibemate reads at most on its next turn", `<input type="number" id="rp-backlog" min="1" max="1000" value="${rs.backlogCap}">`, "Everything posted since its last turn counts, including while it was muted; older messages are dropped with a note in its prompt.")}
|
|
2037
2078
|
</div>`,
|
|
2038
2079
|
"tools, hops, referee, briefs",
|
|
2039
2080
|
)}
|
|
@@ -2057,6 +2098,7 @@
|
|
|
2057
2098
|
$("#rp-dir").value = dir;
|
|
2058
2099
|
$("#rp-dir").dispatchEvent(new Event("change", { bubbles: true }));
|
|
2059
2100
|
}));
|
|
2101
|
+
$("#rp-template").addEventListener("click", () => openSaveTemplateDialog(room));
|
|
2060
2102
|
bindSave($("#rp-form"), $("#rp-save"), async () => {
|
|
2061
2103
|
const name = $("#rp-name").value;
|
|
2062
2104
|
if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
|
|
@@ -2075,6 +2117,7 @@
|
|
|
2075
2117
|
headerRules: $("#rp-header-rules").checked,
|
|
2076
2118
|
showVendorInRoster: $("#rp-vendor").checked,
|
|
2077
2119
|
replayAfterRestart: Number($("#rp-replay").value),
|
|
2120
|
+
backlogCap: Number($("#rp-backlog").value),
|
|
2078
2121
|
refereeAction: $("#rp-referee").value,
|
|
2079
2122
|
turnTaking: $("#rp-turns").value,
|
|
2080
2123
|
waitWhileHumanTypes: $("#rp-wait-typing").checked,
|
|
@@ -2130,6 +2173,13 @@
|
|
|
2130
2173
|
${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>`)}
|
|
2131
2174
|
${field("Reply delay in new rooms, seconds", `<input type="number" id="sp-delay" min="0" max="120" step="0.5" value="${d.replyDelay ?? 4}">`, "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.")}
|
|
2132
2175
|
</div>
|
|
2176
|
+
<div class="section" id="sp-appearance">
|
|
2177
|
+
${sectionTitle("eye", "Appearance")}
|
|
2178
|
+
${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. Everything else in the window scales with it.")}
|
|
2179
|
+
${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.")}
|
|
2180
|
+
${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.")}
|
|
2181
|
+
<div class="bubble" id="sp-chat-sample" style="display:inline-block;font-size:${((s.appearance || {}).chatFontSize || 14.5) / zoomFactor()}px">Messages will read like this, with <code>code</code> a step smaller.</div>
|
|
2182
|
+
</div>
|
|
2133
2183
|
<div class="section" id="sp-editor">
|
|
2134
2184
|
${sectionTitle("pencil", "Open files at a line")}
|
|
2135
2185
|
<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>
|
|
@@ -2231,6 +2281,13 @@
|
|
|
2231
2281
|
});
|
|
2232
2282
|
$("#sp-diagram-color").addEventListener("input", redrawPreview);
|
|
2233
2283
|
renderDiagrams(diagramSection, previewTheme());
|
|
2284
|
+
const sample = $("#sp-chat-sample");
|
|
2285
|
+
$("#sp-chat-fs").addEventListener("input", () => {
|
|
2286
|
+
const px = Number($("#sp-chat-fs").value);
|
|
2287
|
+
if (px >= 12 && px <= 24) sample.style.fontSize = `${px / zoomFactor()}px`;
|
|
2288
|
+
});
|
|
2289
|
+
$("#sp-font").addEventListener("change", () => (sample.style.fontFamily = FONTS.text[$("#sp-font").value].stack));
|
|
2290
|
+
$("#sp-mono").addEventListener("change", () => sample.querySelectorAll("code").forEach((c) => (c.style.fontFamily = FONTS.mono[$("#sp-mono").value].stack)));
|
|
2234
2291
|
bindSave($("#sp-form"), $("#sp-save"), async () => {
|
|
2235
2292
|
const vendorPresets = {};
|
|
2236
2293
|
els.pageInner.querySelectorAll("input[data-vendor]").forEach((inp) => {
|
|
@@ -2242,6 +2299,7 @@
|
|
|
2242
2299
|
agentSkillsNeedApproval: $("#sp-skill-approval").checked,
|
|
2243
2300
|
diagrams: { preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null },
|
|
2244
2301
|
editor: { mode: $("#sp-editor-mode").value, command: $("#sp-editor-cmd").value },
|
|
2302
|
+
appearance: { chatFontSize: Number($("#sp-chat-fs").value), font: $("#sp-font").value, mono: $("#sp-mono").value },
|
|
2245
2303
|
roomDefaults: {
|
|
2246
2304
|
turnTaking: $("#sp-turns").value,
|
|
2247
2305
|
replyDelay: Number($("#sp-delay").value),
|
|
@@ -2802,7 +2860,7 @@
|
|
|
2802
2860
|
const faces = t.vibemates.slice(0, 4).map((v) => avatar({ name: v.name, avatar: v.avatar, color: "#9ca3af" }, 20, {})).join("");
|
|
2803
2861
|
return `<button type="button" class="tpl-item${on ? " on" : ""}" data-id="${esc(t.id)}" role="radio" aria-checked="${on ? "true" : "false"}">
|
|
2804
2862
|
<span class="tpl-emoji">${esc(t.emoji || "🧩")}</span>
|
|
2805
|
-
<span class="tpl-body"><b>${esc(t.name)}${t.recommended ? '<span class="badge tpl-rec">recommended</span>' : ""}</b><span class="tpl-meta">${t.vibemates.length} vibemate${t.vibemates.length === 1 ? "" : "s"}${t.builtin ? " · built in" : "
|
|
2863
|
+
<span class="tpl-body"><b>${esc(t.name)}${t.builtin ? "" : '<span class="badge tpl-mine">your template</span>'}${t.recommended ? '<span class="badge tpl-rec">recommended</span>' : ""}</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>
|
|
2806
2864
|
<span class="tpl-check">${ic("check")}</span>
|
|
2807
2865
|
</button>`;
|
|
2808
2866
|
})
|
|
@@ -2828,10 +2886,14 @@
|
|
|
2828
2886
|
return `<div class="tpl-vm" data-i="${i}">
|
|
2829
2887
|
<div class="tpl-vm-head"><b>${esc(v.name)}</b>${v.tagline ? `<span class="hint">"${esc(v.tagline)}"</span>` : ""}</div>
|
|
2830
2888
|
${v.role ? `<div class="tpl-vm-role">${esc(v.role)}</div>` : ""}
|
|
2889
|
+
${runsOn(v)}
|
|
2831
2890
|
</div>`;
|
|
2832
2891
|
})
|
|
2833
2892
|
.join("")}`;
|
|
2834
|
-
tplEls.detail.insertAdjacentHTML("beforeend",
|
|
2893
|
+
if (t.dir) tplEls.detail.insertAdjacentHTML("beforeend", `<p class="hint">Folder: <code>${esc(t.dir)}</code> (you can change it below)</p>`);
|
|
2894
|
+
tplEls.detail.insertAdjacentHTML("beforeend", t.vibemates.some((v) => v.agentType)
|
|
2895
|
+
? '<p class="hint">A vibemate whose coding agent is not installed here waits in the roster until you pick another.</p>'
|
|
2896
|
+
: '<p class="hint">You pick the coding agent for each vibemate in the room, right after it opens.</p>');
|
|
2835
2897
|
}
|
|
2836
2898
|
$("#tpl-dir-browse").addEventListener("click", () => openFolderPicker(tplEls.dir.value, (dir) => (tplEls.dir.value = dir)));
|
|
2837
2899
|
tplEls.form.addEventListener("submit", async (event) => {
|
|
@@ -2858,6 +2920,142 @@
|
|
|
2858
2920
|
}
|
|
2859
2921
|
});
|
|
2860
2922
|
|
|
2923
|
+
function runsOn(v) {
|
|
2924
|
+
if (!v.agentType) return "";
|
|
2925
|
+
const rec = state.recipes.find((r) => r.id === v.agentType);
|
|
2926
|
+
const parts = [rec ? rec.vendor : v.agentType, v.model, v.effort, v.mode].filter(Boolean);
|
|
2927
|
+
return `<div class="chips tpl-runs">${parts.map((x) => `<span class="chip">${esc(x)}</span>`).join("")}${rec && rec.unavailableReason ? '<span class="badge status-offline">not installed here</span>' : ""}</div>`;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
const stpEls = { dialog: $("#save-template-dialog"), form: $("#save-template-form"), name: $("#stp-name"), desc: $("#stp-desc"), preview: $("#stp-preview"), error: $("#stp-error"), create: $("#stp-create") };
|
|
2931
|
+
const stp = { room: null, template: null };
|
|
2932
|
+
async function openSaveTemplateDialog(room) {
|
|
2933
|
+
stp.room = room;
|
|
2934
|
+
stpEls.error.hidden = true;
|
|
2935
|
+
stpEls.name.value = room.name;
|
|
2936
|
+
stpEls.desc.value = "";
|
|
2937
|
+
stpEls.preview.innerHTML = '<span class="hint">loading…</span>';
|
|
2938
|
+
openDialog(stpEls.dialog);
|
|
2939
|
+
try {
|
|
2940
|
+
const t = (await post(roomApi("/template-preview"), {})).template;
|
|
2941
|
+
stp.template = t;
|
|
2942
|
+
stpEls.preview.innerHTML = renderTemplateForm(t);
|
|
2943
|
+
stpEls.preview.querySelectorAll("[data-stp-browse]").forEach((b) => b.addEventListener("click", () => {
|
|
2944
|
+
const input = $("#stp-dir");
|
|
2945
|
+
openFolderPicker(input.value, (dir) => (input.value = dir));
|
|
2946
|
+
}));
|
|
2947
|
+
stpEls.preview.querySelectorAll("[data-stp-remove]").forEach((b) => b.addEventListener("click", () => {
|
|
2948
|
+
b.closest(".stp-vm").remove();
|
|
2949
|
+
$("#stp-vm-count").textContent = String(stpEls.preview.querySelectorAll(".stp-vm").length);
|
|
2950
|
+
}));
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
stpEls.error.textContent = error.message;
|
|
2953
|
+
stpEls.error.hidden = false;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
const stpField = (label, html, wide) => `<label class="field${wide ? " wide" : ""}"><span class="label">${label}</span>${html}</label>`;
|
|
2957
|
+
const stpSwitch = (label, id, on) => `<label class="switch"><span class="label">${label}</span><input type="checkbox" id="${id}"${on ? " checked" : ""}></label>`;
|
|
2958
|
+
const stpSelect = (id, value, options) => `<select id="${id}">${options.map(([v, l]) => `<option value="${esc(v)}"${String(value) === v ? " selected" : ""}>${esc(l)}</option>`).join("")}</select>`;
|
|
2959
|
+
function renderTemplateForm(t) {
|
|
2960
|
+
const st = t.settings || {};
|
|
2961
|
+
const lang = st.language && st.language.mode === "fixed" ? st.language.language : "";
|
|
2962
|
+
const room = `<div class="stp-grid">
|
|
2963
|
+
${stpField("Emoji", `<input type="text" id="stp-emoji" maxlength="8" value="${esc(t.emoji || "")}" placeholder="none">`)}
|
|
2964
|
+
${stpField("Topic", `<input type="text" id="stp-topic" maxlength="2000" value="${esc(st.topic || "")}" placeholder="what the room is about">`)}
|
|
2965
|
+
${stpField("Language", `<input type="text" id="stp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
2966
|
+
${stpField("Vibemates' own tools", stpSelect("stp-tools", st.tools || "on-request", [["on-request", "Only when someone explicitly asks"], ["never", "Never (chat only)"]]))}
|
|
2967
|
+
${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"]]))}
|
|
2968
|
+
${stpField("Reply delay, seconds", `<input type="number" id="stp-delay" min="0" max="120" step="0.5" value="${esc(String(st.replyDelay ?? 4))}">`)}
|
|
2969
|
+
${stpSwitch("Vibemates wake each other", "stp-wake", st.agentsWakeEachOther !== false)}
|
|
2970
|
+
${stpSwitch("Wait while you are typing", "stp-wait", st.waitWhileHumanTypes !== false)}
|
|
2971
|
+
${stpField("Hop limit", `<input type="number" id="stp-hops" min="0" max="10000" value="${esc(String(st.hopLimit ?? 100))}">`)}
|
|
2972
|
+
${stpField("Max sentences per reply", `<input type="number" id="stp-maxlen" min="1" max="100" value="${st.maxSentences ?? ""}" placeholder="no limit">`)}
|
|
2973
|
+
${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"]]))}
|
|
2974
|
+
${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"]]))}
|
|
2975
|
+
${stpField("Full brief every N turns", `<input type="number" id="stp-brief-turns" min="1" max="10000" value="${esc(String(st.fullBriefEveryTurns ?? 8))}">`)}
|
|
2976
|
+
${stpField("…or every N tokens", `<input type="number" id="stp-brief-tokens" min="1000" max="10000000" step="1000" value="${esc(String(st.fullBriefEveryTokens ?? 20000))}">`)}
|
|
2977
|
+
${stpField("Replay after a reconnect", `<input type="number" id="stp-replay" min="0" max="200" value="${esc(String(st.replayAfterRestart ?? 10))}">`)}
|
|
2978
|
+
${stpField("Missed messages read at most", `<input type="number" id="stp-backlog" min="1" max="1000" value="${esc(String(st.backlogCap ?? 50))}">`)}
|
|
2979
|
+
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
2980
|
+
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
2981
|
+
</div>`;
|
|
2982
|
+
const agentOptions = [["", "none yet: cast when the room opens"], ...state.recipes.map((r) => [r.id, r.vendor + (r.unavailableReason ? " (not installed here)" : "")])];
|
|
2983
|
+
const vms = (t.vibemates || []).map((v, i) => `<div class="stp-vm" data-i="${i}">${avatar({ name: v.name, avatar: v.avatar, color: "#9ca3af" }, 36, {})}<div>
|
|
2984
|
+
<div class="stp-vm-top"><b>${esc(v.name)}</b><button type="button" class="icon-btn sm ghost" title="Leave this vibemate out of the template" data-stp-remove>${ic("close")}</button></div>
|
|
2985
|
+
<div class="stp-vm-fields">
|
|
2986
|
+
${stpField("Vibename", `<input type="text" data-k="name" maxlength="40" value="${esc(v.name)}" required>`)}
|
|
2987
|
+
${stpField("Vibersona", `<input type="text" data-k="tagline" maxlength="80" value="${esc(v.tagline || "")}">`)}
|
|
2988
|
+
${stpField("Vibio", `<textarea data-k="role" rows="3" maxlength="4000">${esc(v.role || "")}</textarea>`, true)}
|
|
2989
|
+
${stpField("Vibeface", `<input type="text" data-k="avatar" maxlength="8" value="${esc(v.avatar || "")}" placeholder="initials">`)}
|
|
2990
|
+
${stpField("Coding agent", stpSelect("", v.agentType || "", agentOptions).replace('id=""', 'data-k="agentType"'))}
|
|
2991
|
+
${stpField("Model", `<input type="text" data-k="model" value="${esc(v.model || "")}" placeholder="the agent's default">`)}
|
|
2992
|
+
${stpField("Effort", `<input type="text" data-k="effort" value="${esc(v.effort || "")}" placeholder="default">`)}
|
|
2993
|
+
${stpField("Mode", `<input type="text" data-k="mode" value="${esc(v.mode || "")}" placeholder="default">`)}
|
|
2994
|
+
${stpField("Reply delay override, s", `<input type="number" data-k="replyDelay" min="0" max="120" step="0.5" value="${v.replyDelay ?? ""}" placeholder="the room's">`)}
|
|
2995
|
+
${stpField("Skills, comma-separated", `<input type="text" data-k="skills" value="${esc((v.skills || []).join(", "))}">`, true)}
|
|
2996
|
+
</div>
|
|
2997
|
+
</div></div>`).join("");
|
|
2998
|
+
return `
|
|
2999
|
+
<div class="stp-section"><h5>Room</h5>${room}</div>
|
|
3000
|
+
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5" maxlength="4000" placeholder="one rule per line">${esc(st.customRules || "")}</textarea></div>
|
|
3001
|
+
<div class="stp-section"><h5>Folder</h5><span class="dir-row"><input type="text" id="stp-dir" maxlength="1000" value="${esc(t.dir || "")}" spellcheck="false"><button type="button" class="btn ghost browse-btn" data-stp-browse>${ic("folder")}Browse</button></span></div>
|
|
3002
|
+
<div class="stp-section"><h5>Vibemates · <span id="stp-vm-count">${(t.vibemates || []).length}</span></h5>${vms || '<span class="hint">none</span>'}</div>`;
|
|
3003
|
+
}
|
|
3004
|
+
function readTemplateForm() {
|
|
3005
|
+
const num = (id) => Number($(id).value);
|
|
3006
|
+
const langText = $("#stp-lang").value.trim();
|
|
3007
|
+
const settings = {
|
|
3008
|
+
topic: $("#stp-topic").value,
|
|
3009
|
+
language: langText ? { mode: "fixed", language: langText } : { mode: "follow-human" },
|
|
3010
|
+
tools: $("#stp-tools").value,
|
|
3011
|
+
turnTaking: $("#stp-turns").value,
|
|
3012
|
+
replyDelay: num("#stp-delay"),
|
|
3013
|
+
agentsWakeEachOther: $("#stp-wake").checked,
|
|
3014
|
+
waitWhileHumanTypes: $("#stp-wait").checked,
|
|
3015
|
+
hopLimit: num("#stp-hops"),
|
|
3016
|
+
maxSentences: $("#stp-maxlen").value === "" ? null : num("#stp-maxlen"),
|
|
3017
|
+
refereeAction: $("#stp-referee").value,
|
|
3018
|
+
humanDescriptionMode: $("#stp-about").value,
|
|
3019
|
+
fullBriefEveryTurns: num("#stp-brief-turns"),
|
|
3020
|
+
fullBriefEveryTokens: num("#stp-brief-tokens"),
|
|
3021
|
+
replayAfterRestart: num("#stp-replay"),
|
|
3022
|
+
backlogCap: num("#stp-backlog"),
|
|
3023
|
+
headerRules: $("#stp-header-rules").checked,
|
|
3024
|
+
showVendorInRoster: $("#stp-vendor").checked,
|
|
3025
|
+
customRules: $("#stp-rules").value.slice(0, 4000),
|
|
3026
|
+
};
|
|
3027
|
+
const vibemates = [...stpEls.preview.querySelectorAll(".stp-vm")].map((row) => {
|
|
3028
|
+
const v = {};
|
|
3029
|
+
row.querySelectorAll("[data-k]").forEach((el) => {
|
|
3030
|
+
const k = el.dataset.k;
|
|
3031
|
+
const val = el.value.trim();
|
|
3032
|
+
if (k === "skills") v.skills = val.split(",").map((x) => x.trim()).filter(Boolean);
|
|
3033
|
+
else if (k === "replyDelay") { if (val !== "") v.replyDelay = Number(val); }
|
|
3034
|
+
else if (val) v[k] = val;
|
|
3035
|
+
});
|
|
3036
|
+
return v;
|
|
3037
|
+
});
|
|
3038
|
+
return { emoji: $("#stp-emoji").value.trim(), dir: $("#stp-dir").value.trim(), settings, vibemates };
|
|
3039
|
+
}
|
|
3040
|
+
stpEls.form.addEventListener("submit", async (event) => {
|
|
3041
|
+
event.preventDefault();
|
|
3042
|
+
if (!stp.room) return;
|
|
3043
|
+
stpEls.create.disabled = true;
|
|
3044
|
+
stpEls.create.textContent = "Creating…";
|
|
3045
|
+
try {
|
|
3046
|
+
const edited = readTemplateForm();
|
|
3047
|
+
const res = await post(`/api/rooms/${encodeURIComponent(stp.room.id)}/save-template`, { name: stpEls.name.value, description: stpEls.desc.value, emoji: edited.emoji, template: { dir: edited.dir, settings: edited.settings, vibemates: edited.vibemates } });
|
|
3048
|
+
closeDialog(stpEls.dialog);
|
|
3049
|
+
toast(`Template "${res.template.name}" created. It is first under "Start from a template".`, "success");
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
stpEls.error.textContent = error.message;
|
|
3052
|
+
stpEls.error.hidden = false;
|
|
3053
|
+
} finally {
|
|
3054
|
+
stpEls.create.disabled = false;
|
|
3055
|
+
stpEls.create.textContent = "Create the template";
|
|
3056
|
+
}
|
|
3057
|
+
});
|
|
3058
|
+
|
|
2861
3059
|
function openRoomDialog() {
|
|
2862
3060
|
els.roomError.hidden = true;
|
|
2863
3061
|
els.roomName.value = "";
|
|
@@ -2953,8 +3151,18 @@
|
|
|
2953
3151
|
}
|
|
2954
3152
|
|
|
2955
3153
|
|
|
3154
|
+
function applyAppearance() {
|
|
3155
|
+
const a = (state.settings || {}).appearance || {};
|
|
3156
|
+
const root = document.documentElement;
|
|
3157
|
+
root.style.zoom = String((a.chatFontSize || 14.5) / 14.5);
|
|
3158
|
+
root.style.setProperty("--font", (FONTS.text[a.font] || FONTS.text.nunito).stack);
|
|
3159
|
+
root.style.setProperty("--mono", (FONTS.mono[a.mono] || FONTS.mono["jetbrains-mono"]).stack);
|
|
3160
|
+
}
|
|
3161
|
+
const zoomFactor = () => Number(document.documentElement.style.zoom) || 1;
|
|
3162
|
+
|
|
2956
3163
|
function loadSnapshot(snapshot) {
|
|
2957
3164
|
state.settings = snapshot.settings;
|
|
3165
|
+
applyAppearance();
|
|
2958
3166
|
state.version = snapshot.version || null;
|
|
2959
3167
|
state.skills = snapshot.skills || [];
|
|
2960
3168
|
state.recipes = snapshot.recipes || [];
|
|
@@ -3131,6 +3339,7 @@
|
|
|
3131
3339
|
});
|
|
3132
3340
|
es.addEventListener("settings", (e) => {
|
|
3133
3341
|
state.settings = JSON.parse(e.data).settings;
|
|
3342
|
+
applyAppearance();
|
|
3134
3343
|
rerenderDiagrams();
|
|
3135
3344
|
renderRail();
|
|
3136
3345
|
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
@@ -3178,14 +3387,14 @@
|
|
|
3178
3387
|
let drag = null;
|
|
3179
3388
|
grip.addEventListener("pointerdown", (e) => {
|
|
3180
3389
|
if (e.button !== 0) return;
|
|
3181
|
-
drag = { y: e.clientY, h: els.input.offsetHeight };
|
|
3390
|
+
drag = { y: e.clientY / zoomFactor(), h: els.input.offsetHeight };
|
|
3182
3391
|
grip.setPointerCapture(e.pointerId);
|
|
3183
3392
|
els.composer.classList.add("resizing");
|
|
3184
3393
|
e.preventDefault();
|
|
3185
3394
|
});
|
|
3186
3395
|
grip.addEventListener("pointermove", (e) => {
|
|
3187
3396
|
if (!drag) return;
|
|
3188
|
-
composerMin = Math.round(Math.min(composerCeiling(), Math.max(36, drag.h + drag.y - e.clientY)));
|
|
3397
|
+
composerMin = Math.round(Math.min(composerCeiling(), Math.max(36, drag.h + drag.y - e.clientY / zoomFactor())));
|
|
3189
3398
|
autosize();
|
|
3190
3399
|
});
|
|
3191
3400
|
const stop = () => {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Fira Code"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/fira-code-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/inter-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Noto Sans"; font-style: normal; font-weight: 500 800; font-display: swap; src: url("/fonts/noto-sans-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
|
+
@font-face { font-family: "Source Code Pro"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/source-code-pro-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
|
|
3
|
+
@font-face { font-family: "Source Code Pro"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/source-code-pro-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
|
4
|
+
@font-face { font-family: "Source Code Pro"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/source-code-pro-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
|
5
|
+
@font-face { font-family: "Source Code Pro"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("/fonts/source-code-pro-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
package/ui/index.html
CHANGED
|
@@ -164,6 +164,17 @@
|
|
|
164
164
|
<div class="actions"><button type="button" class="btn ghost" data-close>Cancel</button><button type="submit" class="btn primary" id="tpl-create">Create the room</button></div>
|
|
165
165
|
</form>
|
|
166
166
|
</dialog>
|
|
167
|
+
<dialog id="save-template-dialog" class="dialog wide">
|
|
168
|
+
<form id="save-template-form" method="dialog">
|
|
169
|
+
<h3><span class="h-ico" data-icon="rooms"></span>Turn this room into a template</h3>
|
|
170
|
+
<p class="lead">Everything below goes into the template: the room's settings and rules, its folder, and each vibemate with the coding agent it runs on. Check it, name it, create it.</p>
|
|
171
|
+
<label>Template name<input id="stp-name" type="text" maxlength="60" required placeholder="e.g. Payments refactor"></label>
|
|
172
|
+
<label>Description <span class="hint">(optional)</span><textarea id="stp-desc" rows="2" maxlength="400" placeholder="what this setup is good for"></textarea></label>
|
|
173
|
+
<div class="stp-preview" id="stp-preview"></div>
|
|
174
|
+
<p class="error" id="stp-error" hidden></p>
|
|
175
|
+
<div class="actions"><button type="button" class="btn ghost" data-close>Cancel</button><button type="submit" class="btn primary" id="stp-create">Create the template</button></div>
|
|
176
|
+
</form>
|
|
177
|
+
</dialog>
|
|
167
178
|
<dialog id="invite-dialog" class="dialog wide">
|
|
168
179
|
<form id="invite-form" method="dialog">
|
|
169
180
|
<h3><span class="h-ico" data-icon="spark"></span>Summon a Vibemate</h3>
|
package/ui/theme.css
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
|
|
2
2
|
@import url("/fonts/nunito.css");
|
|
3
|
+
@import url("/fonts/inter.css");
|
|
4
|
+
@import url("/fonts/noto-sans.css");
|
|
5
|
+
@import url("/fonts/jetbrains-mono.css");
|
|
6
|
+
@import url("/fonts/fira-code.css");
|
|
7
|
+
@import url("/fonts/source-code-pro.css");
|
|
3
8
|
:root {
|
|
4
9
|
--primary: #5b5bf0;
|
|
5
10
|
--primary-deep: #4f4fe0;
|
|
@@ -48,6 +53,25 @@
|
|
|
48
53
|
--mine-border: transparent;
|
|
49
54
|
--text: var(--ink);
|
|
50
55
|
--ok: var(--green);
|
|
56
|
+
--orchid: #f3e4fb;
|
|
57
|
+
--orchid-ink: #8a3fc2;
|
|
58
|
+
--st-ready: var(--mint); --st-ready-ink: var(--mint-ink); --st-ready-dot: var(--green);
|
|
59
|
+
--st-waiting: var(--sky); --st-waiting-ink: #2a6fbf; --st-waiting-dot: #7aa7ff;
|
|
60
|
+
--st-thinking: var(--warm); --st-thinking-ink: var(--warm-ink); --st-thinking-dot: var(--warn);
|
|
61
|
+
--st-writing: var(--orchid); --st-writing-ink: var(--orchid-ink); --st-writing-dot: #b06be0;
|
|
62
|
+
--st-error: var(--rose); --st-error-ink: var(--rose-ink); --st-error-dot: var(--rose-ink);
|
|
63
|
+
--st-asleep: var(--softer); --st-asleep-ink: var(--muted); --st-asleep-dot: var(--faint);
|
|
64
|
+
--attention: #ff8a3d;
|
|
65
|
+
--attention-ink: #c9461c;
|
|
66
|
+
--attention-grad: linear-gradient(135deg, #ff9447, #e8642a);
|
|
67
|
+
--attention-shadow: 0 8px 18px -10px rgba(232, 100, 42, 0.7);
|
|
68
|
+
--unread-grad: linear-gradient(135deg, #ff7a45, #f0452c);
|
|
69
|
+
--note-hover: #ffeaa8;
|
|
70
|
+
--tick-mine: #cdcdf9;
|
|
71
|
+
--tick-mine-view: #a9a9f5;
|
|
72
|
+
--tick-fallback: #9ca3af;
|
|
73
|
+
--unseen-ink: #be185d;
|
|
74
|
+
--unseen-bg: #fff1f7;
|
|
51
75
|
--r-xs: 8px;
|
|
52
76
|
--r-sm: 12px;
|
|
53
77
|
--r-md: 14px;
|
|
@@ -292,35 +316,37 @@ p .geek-tip, .hint > .geek-tip, .switch .label > .geek-tip { margin-left: 8px; }
|
|
|
292
316
|
.agent-tile.off { opacity: 0.5; filter: grayscale(1); cursor: default; }
|
|
293
317
|
.badge { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; padding: 3px 10px; border-radius: var(--r-pill); background: var(--lav); color: var(--primary); font-weight: 800; white-space: nowrap; vertical-align: middle; }
|
|
294
318
|
.badge.outline { background: transparent; border: 2px solid var(--lav-2); color: var(--muted); }
|
|
295
|
-
.badge.status-idle { background: var(--
|
|
296
|
-
.badge.status-thinking { background: var(--
|
|
297
|
-
.badge.status-
|
|
298
|
-
.badge.status-
|
|
299
|
-
.badge.status-
|
|
300
|
-
.badge.status-
|
|
301
|
-
.badge.status-
|
|
319
|
+
.badge.status-idle { background: var(--st-ready); color: var(--st-ready-ink); }
|
|
320
|
+
.badge.status-thinking { background: var(--st-thinking); color: var(--st-thinking-ink); }
|
|
321
|
+
.badge.status-writing { background: var(--st-writing); color: var(--st-writing-ink); }
|
|
322
|
+
.badge.status-queued { background: var(--st-waiting); color: var(--st-waiting-ink); }
|
|
323
|
+
.badge.status-starting { background: var(--st-waiting); color: var(--st-waiting-ink); }
|
|
324
|
+
.badge.status-offline { background: var(--st-asleep); color: var(--st-asleep-ink); }
|
|
325
|
+
.badge.status-error { background: var(--st-error); color: var(--st-error-ink); }
|
|
326
|
+
.badge.status-left { background: var(--st-asleep); color: var(--st-asleep-ink); }
|
|
302
327
|
.badge.muted { background: var(--softer); color: var(--muted); }
|
|
303
328
|
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
304
|
-
.badge.status-thinking .dot { animation: pulse 1.2s ease-in-out infinite; }
|
|
329
|
+
.badge.status-thinking .dot, .badge.status-writing .dot { animation: pulse 1.2s ease-in-out infinite; }
|
|
305
330
|
.chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 800; background: var(--lav); color: var(--primary); border: 0; border-radius: var(--r-pill); padding: 4px 10px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
306
331
|
.chip .i { width: 13px; height: 13px; }
|
|
307
|
-
.chip-completed { background: var(--
|
|
308
|
-
.chip-failed { background: var(--
|
|
309
|
-
.chip-in_progress { background: var(--
|
|
332
|
+
.chip-completed { background: var(--st-ready); color: var(--st-ready-ink); }
|
|
333
|
+
.chip-failed { background: var(--st-error); color: var(--st-error-ink); }
|
|
334
|
+
.chip-in_progress { background: var(--st-thinking); color: var(--st-thinking-ink); }
|
|
310
335
|
.count-pill { background: var(--primary); color: #fff; border-radius: var(--r-pill); font-size: 10px; padding: 1px 7px; font-weight: 800; line-height: 15px; }
|
|
311
336
|
.zzz { font-size: 10px; font-weight: 800; color: var(--muted); background: var(--softer); padding: 3px 8px; border-radius: var(--r-pill); }
|
|
312
|
-
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--
|
|
337
|
+
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--st-thinking-dot); box-shadow: 0 0 0 3px var(--st-thinking); animation: pulse 1.2s ease-in-out infinite; display: inline-block; }
|
|
313
338
|
.avatar { position: relative; display: inline-block; flex: none; }
|
|
314
339
|
.avatar .av-tile { display: grid; place-items: center; width: 100%; height: 100%; font-weight: 800; line-height: 1; font-family: var(--font); user-select: none; }
|
|
315
340
|
.avatar-badge { position: absolute; right: -4px; bottom: -4px; width: 44%; height: 44%; min-width: 15px; min-height: 15px; border-radius: 6px; background: #fff; border: 2px solid #fff; box-shadow: var(--shadow-tile); display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 800; color: var(--primary); overflow: hidden; }
|
|
316
341
|
.avatar-badge img { width: 72%; height: 72%; object-fit: contain; }
|
|
317
342
|
.avatar-status { position: absolute; left: -3px; bottom: -3px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #fff; background: var(--faint); }
|
|
318
|
-
.avatar-status.status-idle { background: var(--
|
|
319
|
-
.avatar-status.status-thinking { background: var(--
|
|
320
|
-
.avatar-status.status-
|
|
321
|
-
.avatar-status.status-
|
|
322
|
-
.avatar-status.status-
|
|
323
|
-
.avatar-status.status-
|
|
343
|
+
.avatar-status.status-idle { background: var(--st-ready-dot); }
|
|
344
|
+
.avatar-status.status-thinking { background: var(--st-thinking-dot); animation: pulse 1.2s ease-in-out infinite; }
|
|
345
|
+
.avatar-status.status-writing { background: var(--st-writing-dot); animation: pulse 1.2s ease-in-out infinite; }
|
|
346
|
+
.avatar-status.status-offline { background: var(--st-asleep-dot); }
|
|
347
|
+
.avatar-status.status-error { background: var(--st-error-dot); }
|
|
348
|
+
.avatar-status.status-starting { background: var(--st-waiting-dot); }
|
|
349
|
+
.avatar-status.status-queued { background: var(--st-waiting-dot); }
|
|
324
350
|
.avatar-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); gap: 6px; margin: 4px 0 8px; max-height: 148px; overflow-y: auto; padding: 2px 4px 2px 2px; scroll-padding: 4px; }
|
|
325
351
|
.avatar-picker button { aspect-ratio: 1; width: 100%; border-radius: 12px; border: 2px solid transparent; background: var(--soft); font-size: 20px; padding: 0; display: inline-flex; align-items: center; justify-content: center; transition: transform var(--t-fast) var(--ease-pop), border-color var(--t-fast), background var(--t-fast); }
|
|
326
352
|
.avatar-picker button:hover { transform: translateY(-1px); background: var(--lav); }
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Forge & Lumen",
|
|
3
|
-
"order": 1,
|
|
4
|
-
"emoji": "⚒️",
|
|
5
|
-
"recommended": true,
|
|
6
|
-
"description": "Two equals around one codebase. Forge builds by default, Lumen explains by default; either does either when asked, and either reviews the other's work when you ask for it. The way viberoom itself was built.",
|
|
7
|
-
"settings": {
|
|
8
|
-
"language": {
|
|
9
|
-
"mode": "follow-human"
|
|
10
|
-
},
|
|
11
|
-
"turnTaking": "parallel",
|
|
12
|
-
"agentsWakeEachOther": false,
|
|
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. 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
|
-
},
|
|
16
|
-
"vibemates": [
|
|
17
|
-
{
|
|
18
|
-
"name": "Forge",
|
|
19
|
-
"tagline": "builds by default; explains and reviews when asked",
|
|
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
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"name": "Lumen",
|
|
24
|
-
"tagline": "explains by default; builds and reviews when asked",
|
|
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
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|