viberoom 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
6
+ export const SHIPPED_TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
7
+ export function cleanTemplate(raw, id) {
8
+ const t = (raw ?? {});
9
+ const name = String(t.name ?? "").trim();
10
+ if (!name)
11
+ throw new Error("name is required");
12
+ const list = Array.isArray(t.vibemates) ? t.vibemates : [];
13
+ const vibemates = list.map((v, i) => {
14
+ const o = (v ?? {});
15
+ const vname = String(o.name ?? "").trim();
16
+ if (!vname)
17
+ throw new Error(`vibemate ${i + 1} has no name`);
18
+ const out = { name: vname };
19
+ for (const key of ["tagline", "role", "avatar", "agentType", "model", "effort", "mode"]) {
20
+ if (typeof o[key] === "string" && o[key].trim())
21
+ out[key] = o[key].trim();
22
+ }
23
+ if (Array.isArray(o.skills))
24
+ out.skills = o.skills.map((s) => String(s).trim()).filter(Boolean);
25
+ return out;
26
+ });
27
+ const settings = (t.settings && typeof t.settings === "object" ? t.settings : {});
28
+ const out = { id, name, description: String(t.description ?? "").trim(), settings, vibemates };
29
+ if (typeof t.order === "number" && Number.isFinite(t.order))
30
+ out.order = t.order;
31
+ if (typeof t.emoji === "string" && t.emoji.trim())
32
+ out.emoji = t.emoji.trim().slice(0, 8);
33
+ if (t.recommended === true)
34
+ out.recommended = true;
35
+ return out;
36
+ }
37
+ function readTemplates(dir, log, builtin) {
38
+ const out = [];
39
+ if (!existsSync(dir))
40
+ return out;
41
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
42
+ if (!entry.isDirectory() || !ID_PATTERN.test(entry.name))
43
+ continue;
44
+ const file = join(dir, entry.name, "template.json");
45
+ if (!existsSync(file))
46
+ continue;
47
+ try {
48
+ const t = cleanTemplate(JSON.parse(readFileSync(file, "utf8")), entry.name);
49
+ if (builtin)
50
+ t.builtin = true;
51
+ out.push(t);
52
+ }
53
+ catch (error) {
54
+ log.warn(`template ${entry.name} in ${dir} skipped: ${error instanceof Error ? error.message : String(error)}`);
55
+ }
56
+ }
57
+ return out.sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.name.localeCompare(b.name));
58
+ }
59
+ export class TemplateLibrary {
60
+ dir;
61
+ log;
62
+ shippedDir;
63
+ constructor(dir, log, shippedDir = SHIPPED_TEMPLATES_DIR) {
64
+ this.dir = dir;
65
+ this.log = log;
66
+ this.shippedDir = shippedDir;
67
+ }
68
+ list() {
69
+ const own = readTemplates(this.dir, this.log, false);
70
+ const taken = new Set(own.map((t) => t.id));
71
+ return [...readTemplates(this.shippedDir, this.log, true).filter((t) => !taken.has(t.id)), ...own];
72
+ }
73
+ get(id) {
74
+ return this.list().find((t) => t.id === id);
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberoom",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "viberoom: group chat rooms for a human and several coding agents over the Agent Client Protocol",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,6 +24,7 @@
24
24
  "dist",
25
25
  "vendor",
26
26
  "ui",
27
+ "templates",
27
28
  "assets",
28
29
  "scripts",
29
30
  "NOTICE"
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "Design critique",
3
+ "order": 40,
4
+ "emoji": "⚖️",
5
+ "description": "Two vibemates argue a design in front of you: a Proposer who makes the strongest case for an approach, and a Skeptic who finds what breaks it. They read the code; they do not change it. You decide.",
6
+ "settings": {
7
+ "customRules": "Neither vibemate changes files; this room is for reading and arguing.\nEvery claim about the code names a file and a line.\nDisagreements end with the concrete condition under which each side would be right.",
8
+ "agentsWakeEachOther": true,
9
+ "turnTaking": "one-at-a-time",
10
+ "hopLimit": 12
11
+ },
12
+ "vibemates": [
13
+ {
14
+ "name": "Proposer",
15
+ "tagline": "makes the strongest case for a design",
16
+ "role": "You propose designs and defend them with evidence from the code. State the approach, its cost, and what it makes easy. When the Skeptic finds a real hole, concede it and adjust the proposal rather than restating it. You read code; you never edit it."
17
+ },
18
+ {
19
+ "name": "Skeptic",
20
+ "tagline": "finds what breaks it",
21
+ "role": "You look for what breaks a proposed design: the edge case, the migration cost, the thing that exists in the code and contradicts the plan. Name files and lines. Be specific and brief; a hole is worth more than a list of doubts. When a proposal survives, say so. You read code; you never edit it."
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "Explainer",
3
+ "order": 30,
4
+ "emoji": "📖",
5
+ "description": "One vibemate that explains a codebase and never changes it: how a thing works, where it lives, why it is that way. Ask it anything about the folder you point it at.",
6
+ "settings": {
7
+ "customRules": "Never edit, create or delete files; this room is read-only.\nAnswer with file paths and line numbers; quote the code, do not paraphrase it.\nWhen something is unknown or unverified, say so instead of guessing.",
8
+ "tools": "on-request"
9
+ },
10
+ "vibemates": [
11
+ {
12
+ "name": "Explainer",
13
+ "tagline": "explains the code, never touches it",
14
+ "role": "You explain how this codebase works. Read whatever you need, then answer in plain language with file paths and line numbers, quoting the code when it settles a question. Structure long answers as a short walk through the flow. You never edit, create or delete files."
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,28 @@
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.\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. Either of you builds, explains or reviews, whatever the human asks; without an address, a task goes to Forge and a question to Lumen. The human decides when work is reviewed; do not review, comment on or extend the other's work unless asked, and do not answer the other's report unless it asks you something. When you build, read the code first, keep the change small, and report to the human — never to the other vibemate — 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 and what is left. When asked to review, first say in one line what you will check, then review by that criterion, not an impression: a number, a live check, the risky case. Say no with a reason and a cheaper alternative; never concede to be agreeable, never object to seem rigorous. Work only on your own task, never on the other's; do not touch what the other is working on. On a message to everyone, answer only if it concerns you; never add to, correct or comment on the other's reply unless the human asks. Before building something, check whether the other already did it."
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. Either of you builds, explains or reviews, whatever the human asks; without an address, a task goes to Forge and a question to Lumen. The human decides when work is reviewed; do not review, comment on or extend the other's work unless asked, and do not answer the other's report unless it asks you something. When you build, read the code first, keep the change small, and report to the human — never to the other vibemate — 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 and what is left. When asked to review, first say in one line what you will check, then review by that criterion, not an impression: a number, a live check, the risky case. Say no with a reason and a cheaper alternative; never concede to be agreeable, never object to seem rigorous. Work only on your own task, never on the other's; do not touch what the other is working on. On a message to everyone, answer only if it concerns you; never add to, correct or comment on the other's reply unless the human asks. Before building something, check whether the other already did it."
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "Pair programmer",
3
+ "order": 20,
4
+ "emoji": "🧑‍💻",
5
+ "description": "One vibemate at your side on a codebase: it reads before it writes, changes one thing at a time, runs what it changed, and tells you what it would do next instead of doing it. You drive.",
6
+ "settings": {
7
+ "customRules": "One change at a time; run or test it before reporting.\nAsk before touching anything outside the folder you were pointed at.\nEnd every report with the next step you propose, not started.",
8
+ "turnTaking": "one-at-a-time"
9
+ },
10
+ "vibemates": [
11
+ {
12
+ "name": "Pair",
13
+ "tagline": "codes with you, one step at a time",
14
+ "role": "You are a pair programmer. The human drives: you read the code first, make one small change at a time, run or test it, and report what changed with file paths. Propose the next step; do not take it until asked. When unsure, ask one precise question rather than guessing."
15
+ }
16
+ ]
17
+ }
package/ui/app.css CHANGED
@@ -20,7 +20,24 @@
20
20
  .rail-logo .mark { width: 44px; height: 44px; border-radius: 14px; flex: none; display: block; box-shadow: 0 8px 18px -6px rgba(91, 91, 240, 0.6); }
21
21
  .rail-logo .word, .rail-item .label, .rail .conn-label { opacity: 0; width: 0; overflow: hidden; transition: opacity var(--t-base) var(--ease-out), width var(--t-base) var(--ease-out); white-space: nowrap; }
22
22
  .shell.rail-open .rail-logo .word, .shell.rail-open .rail-item .label, .shell.rail-open .rail .conn-label { opacity: 1; width: auto; }
23
- .rail-items { display: flex; flex-direction: column; gap: 10px; align-items: center; }
23
+ .rail-items { display: flex; flex-direction: column; gap: 10px; align-items: center; flex: 1 1 auto; min-height: 0; }
24
+ .rail-rooms-wrap { display: flex; flex-direction: column; align-items: stretch; flex: 0 1 auto; min-height: 0; align-self: stretch; margin: 2px 0; }
25
+ .rail-hr { flex: none; height: 1px; width: 44px; margin: 0 auto; background: linear-gradient(90deg, transparent, var(--lav-2) 25%, var(--lav-2) 75%, transparent); }
26
+ .shell.rail-open .rail-hr { width: auto; margin: 0 4px; }
27
+ .rail-rooms { display: flex; flex-direction: column; gap: 10px; align-items: center; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable both-edges; padding: 10px 0; }
28
+ .rail-rooms::-webkit-scrollbar { width: 4px; }
29
+ .rail-rooms::-webkit-scrollbar-track { margin: 10px 0; }
30
+ @supports not selector(::-webkit-scrollbar) { .rail-rooms { scrollbar-width: thin; } }
31
+ .shell.rail-open .rail-rooms { align-items: stretch; }
32
+ .rail-room { flex: none; }
33
+ .rail-room .room-mark { width: 40px; height: 40px; border-radius: 13px; font-size: 16px; opacity: 0.72; transition: opacity var(--t-fast), transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast); }
34
+ .rail-room .room-mark.emoji { font-size: 21px; }
35
+ .rail-room:hover .room-mark { opacity: 1; transform: translateY(-1px); }
36
+ .rail-room.active, .rail-room.active:hover { background: transparent; box-shadow: none; }
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: linear-gradient(135deg, #ff7a45, #f0452c); min-width: 18px; text-align: center; z-index: 2; }
39
+ .rail-room .rail-count.bump { animation: badge-pop 360ms var(--ease-pop); }
40
+ @keyframes badge-pop { 0% { transform: scale(0.6); } 55% { transform: scale(1.3); } 100% { transform: scale(1); } }
24
41
  .shell.rail-open .rail-items { align-items: stretch; padding: 0 14px; }
25
42
  .shell:not(.rail-open) .rail-item, .shell:not(.rail-open) .rail-logo { gap: 0; }
26
43
  .rail-item.new:hover { background: #fff; color: var(--primary); box-shadow: var(--shadow-tile); }
@@ -179,24 +196,65 @@
179
196
  .chat-actions .search { margin: 0; width: 250px; }
180
197
  .messages { position: relative; flex: 1; overflow-y: auto; padding: 12px 22px; display: flex; flex-direction: column; gap: 14px; background: var(--canvas-pattern) content-box, var(--grad-canvas); background-size: var(--canvas-pattern-size), auto; }
181
198
  .day { align-self: center; color: var(--faint); font-size: 11px; font-weight: 800; padding: 3px 12px; margin: 2px 0; }
182
- .msg { display: flex; gap: 12px; align-items: flex-start; max-width: 100%; animation: bubble-in var(--t-base) var(--ease-out); }
183
- .msg.agent { padding-right: 64px; }
184
- .msg.mine { flex-direction: row-reverse; padding-left: 64px; }
199
+ .msg { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; max-width: 100%; animation: bubble-in var(--t-base) var(--ease-out); }
200
+ .messages .msg { content-visibility: auto; contain-intrinsic-size: auto 120px; flex-shrink: 0; }
201
+ .msg.agent { padding-right: 40px; }
202
+ .msg.mine { align-items: flex-end; padding-left: 40px; }
203
+ .head-av { display: inline-flex; width: 32px; height: 32px; padding: 4px; box-sizing: content-box; flex: none; }
204
+ .msg.agent .bubble, .msg.agent .shots { margin-left: 16px; }
205
+ .msg.mine .bubble, .msg.mine .shots { margin-right: 16px; }
206
+ .msg.mine .head { flex-direction: row-reverse; }
185
207
  .msg.system { justify-content: center; }
186
208
  .msg.hidden-by-search { display: none; }
187
209
  .sys { color: var(--muted); font-size: 11px; font-weight: 700; padding: 2px 10px; max-width: 80%; text-align: center; }
188
210
  .sys.warn { color: var(--warm-ink); background: var(--warm); border-radius: var(--r-pill); padding: 6px 12px; font-weight: 800; }
189
- .bubble-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; max-width: min(1040px, 100%); }
211
+ .bubble-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; width: 100%; max-width: min(1040px, 100%); }
190
212
  .msg.mine .bubble-col { align-items: flex-end; }
191
213
  .shell.side-collapsed .bubble-col { max-width: min(1480px, 100%); }
192
- .timeline { position: absolute; right: 6px; top: 78px; bottom: 96px; width: 18px; z-index: 4; }
214
+ .timeline { position: absolute; right: 6px; top: 78px; bottom: calc(var(--composer-h, 60px) + 36px); width: 18px; z-index: 4; }
193
215
  .tl-ticks { position: absolute; inset: 0; }
194
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; }
195
217
  .tl-tick { position: absolute; left: 3px; width: 12px; height: 5px; border-radius: 3px; background: #cdcdf9; cursor: pointer; transition: background var(--t-fast), transform var(--t-fast); }
196
218
  .tl-tick:hover, .tl-tick.active { background: var(--primary); transform: scaleX(1.25); }
197
219
  .tl-tick.in-view { background: #a9a9f5; }
220
+ .tl-tick.pinned { background: var(--primary); }
221
+ .tl-row.current.pinned { background: var(--lav-2); }
222
+ .tl-row.pinned { opacity: 1; color: var(--ink); }
223
+ .tl-row .tl-pin { flex: none; margin-left: auto; align-self: center; color: var(--primary); }
224
+ .tl-row .tl-pin .i { width: 14px; height: 14px; }
225
+ .timeline.left { left: 6px; right: auto; }
226
+ .timeline.left .tl-view { background: rgba(28, 27, 51, 0.05); }
227
+ .timeline.left .tl-tick { background: color-mix(in srgb, var(--tick, #9ca3af) 35%, #fff); }
228
+ .timeline.left .tl-tick.in-view { background: color-mix(in srgb, var(--tick, #9ca3af) 60%, #fff); }
229
+ .timeline.left .tl-tick.pinned, .timeline.left .tl-tick:hover, .timeline.left .tl-tick.active { background: var(--tick, #9ca3af); }
230
+ .timeline.left .tl-pop { left: 26px; right: auto; }
231
+ .tl-pop::before { content: ""; position: absolute; top: -6px; bottom: -6px; right: -6px; width: 6px; }
232
+ .timeline.left .tl-pop::before { right: auto; left: -6px; }
233
+ .tl-av { flex: none; width: 16px; height: 16px; margin-top: 1px; }
234
+ .chat-head { position: relative; }
235
+ .working-now { position: absolute; left: 22px; bottom: -13px; display: flex; align-items: flex-end; gap: 6px; margin: 0; z-index: 6; }
236
+ .wn-av { border: 0; padding: 2px; background: var(--card); border-radius: 50%; box-shadow: var(--shadow-pop); cursor: pointer; line-height: 0; animation: wn-bob 1.6s ease-in-out infinite; animation-delay: calc(var(--i, 0) * 0.28s); }
237
+ .wn-av[hidden] { display: none; }
238
+ @keyframes wn-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
239
+ .timeline .tl-tick.pinned.i { width: 12px; height: 6px; left: 3px; border-radius: 3px; -webkit-mask: none; mask: none; background: var(--tick, var(--primary)); overflow: visible; }
240
+ .timeline .tl-tick.pinned::after { content: ""; position: absolute; width: 16px; height: 16px; top: -11px; background: var(--tick, var(--primary)); -webkit-mask: var(--icon) center / contain no-repeat; mask: var(--icon) center / contain no-repeat; filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 1px #fff); pointer-events: none; }
241
+ .timeline.left .tl-tick.pinned::after { left: -7px; transform: rotate(-45deg); }
242
+ .timeline:not(.left) .tl-tick.pinned::after { right: -7px; transform: rotate(45deg); }
243
+ .timeline .tl-tick.pinned:hover, .timeline .tl-tick.pinned.active { transform: none; filter: brightness(0.85); }
244
+ .pins-chip { cursor: pointer; border: 0; font: inherit; font-size: 12px; font-weight: 800; color: var(--primary); background: var(--lav); }
245
+ .pins-chip:hover { background: var(--lav-2); }
246
+ .pins-panel { position: absolute; right: 22px; top: 62px; width: 380px; max-height: 60%; overflow: auto; background: var(--card); border-radius: 14px; box-shadow: var(--shadow-pop); padding: 6px; z-index: 30; animation: rise var(--t-base) var(--ease-out); }
247
+ .pin-row { align-items: center; }
248
+ .pin-row .tl-text { flex: 1; order: 1; }
249
+ .pin-row .pin-time { order: 2; }
250
+ .pin-row .pin-x { order: 4; }
251
+ .pin-row.mine .tl-av { order: 3; }
252
+ .pin-time { color: var(--faint); font-size: 11px; font-weight: 600; white-space: nowrap; }
253
+ .pin-x { border: 0; background: transparent; color: var(--muted); font-size: 16px; line-height: 1; padding: 0 4px; cursor: pointer; border-radius: 6px; }
254
+ .pin-x:hover { color: var(--danger); background: var(--soft); }
198
255
  .tl-pop { position: absolute; right: 26px; width: 340px; background: var(--card); border-radius: 14px; box-shadow: var(--shadow-pop); padding: 6px; z-index: 30; animation: rise var(--t-base) var(--ease-out); }
199
- .tl-row { padding: 6px 10px; border-radius: 9px; font-size: 12.5px; font-weight: 600; line-height: 1.4; color: var(--ink-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; cursor: pointer; }
256
+ .tl-row { padding: 6px 10px; border-radius: 9px; font-size: 12.5px; font-weight: 600; line-height: 1.4; color: var(--ink-2); display: flex; gap: 8px; align-items: flex-start; cursor: pointer; }
257
+ .tl-text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; }
200
258
  .tl-row.faded { color: var(--muted); opacity: 0.8; }
201
259
  .tl-row.far { opacity: 0.5; }
202
260
  .tl-row.current { background: var(--lav); color: var(--primary); font-weight: 800; }
@@ -205,15 +263,32 @@
205
263
  @keyframes tl-flash { 0% { box-shadow: 0 0 0 0 rgba(91, 91, 240, 0.55); } 100% { box-shadow: 0 0 0 14px rgba(91, 91, 240, 0); } }
206
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); }
207
265
  .jump-latest:hover { background: var(--soft); }
208
- .jump-latest svg { width: 16px; height: 16px; }
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: stretch; max-width: 230px; border-radius: 14px; background: var(--warm); color: var(--warm-ink); box-shadow: var(--shadow-pop); animation: rise var(--t-base) var(--ease-out); overflow: hidden; }
268
+ .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
+ .done-go:hover { background: #ffeaa8; }
270
+ .done-go .avatar { flex: none; }
271
+ .done-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
272
+ .done-text b { font-size: 12px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
273
+ .done-text small { font-size: 11px; font-weight: 600; opacity: 0.8; white-space: nowrap; }
274
+ .done-x { border: 0; background: transparent; color: inherit; opacity: 0.6; font-size: 15px; line-height: 1; padding: 0 9px; cursor: pointer; }
275
+ .done-x:hover { opacity: 1; background: #ffeaa8; }
276
+ .jump-latest .i { width: 16px; height: 16px; }
209
277
  .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); }
210
278
  .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; }
211
279
  .msg.mine .bubble .text { color: #fff; }
280
+ .msg.mine.waiting .bubble { background: linear-gradient(135deg, #ff9447, #e8642a); box-shadow: 0 8px 18px -10px rgba(232, 100, 42, 0.7); }
281
+ .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); }
282
+ .bubble .waiting .i { width: 14px; height: 14px; vertical-align: -2px; }
283
+ .bubble .waiting-text { flex: 1; min-width: 160px; }
284
+ .bubble .waiting-stop { border: 0; border-radius: 10px; padding: 6px 12px; background: rgba(255, 255, 255, 0.92); color: #c9461c; font-weight: 800; font-size: 12.5px; cursor: pointer; }
285
+ .bubble .waiting-stop:hover:not(:disabled) { background: #fff; }
286
+ .bubble .waiting-stop:disabled { opacity: 0.7; cursor: default; }
212
287
  .msg.mine .mention { background: rgba(255, 255, 255, 0.22); color: #fff !important; padding: 1px 7px; border-radius: 6px; font-weight: 800; }
213
288
  .msg.mine .bubble code { background: rgba(255, 255, 255, 0.18); color: #fff; }
214
289
  .msg.mine .open-link { color: #fff; text-decoration-color: rgba(255, 255, 255, 0.5); }
215
290
  .bubble .head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 2px; }
216
- .head { display: flex; gap: 8px; align-items: baseline; padding: 0 4px; }
291
+ .head { display: flex; gap: 8px; align-items: center; padding: 0 4px; }
217
292
  .head .name { font-weight: 800; font-size: 14px; }
218
293
  .head .time { color: var(--faint); font-size: 11px; font-weight: 600; cursor: default; white-space: nowrap; }
219
294
  .msg.mine .head { justify-content: flex-end; }
@@ -258,7 +333,7 @@
258
333
  .tn.selected { background: var(--lav-2); color: var(--primary); }
259
334
  .tn.hidden-dir { color: var(--muted); font-weight: 600; }
260
335
  .tn-tw { flex: 0 0 18px; width: 18px; height: 18px; border: 0; background: transparent; color: var(--muted); padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 5px; transition: transform var(--t-fast); }
261
- .tn-tw svg { width: 14px; height: 14px; }
336
+ .tn-tw .i { width: 14px; height: 14px; }
262
337
  .tn-tw:hover { background: var(--card); color: var(--primary); }
263
338
  .tn.open .tn-tw { transform: rotate(90deg); }
264
339
  .tn.leaf .tn-tw { visibility: hidden; }
@@ -294,6 +369,14 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
294
369
  .agent-notices:empty, .tools:empty, .meta:empty, .perms:empty { display: none; }
295
370
  .agent-notice { font-size: 12px; font-weight: 700; color: var(--warm-ink); background: var(--warm); border-radius: 10px; padding: 5px 10px; margin: 2px 0 8px; display: flex; gap: 6px; align-items: flex-start; }
296
371
  .tools { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
372
+ .tool { display: inline-flex; flex-direction: column; max-width: 100%; min-width: 0; }
373
+ .tool.open { flex-basis: 100%; }
374
+ .tool > .chip { font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; text-align: left; }
375
+ .tool-body { margin: 4px 0 2px; padding: 8px 10px; background: var(--soft); border-radius: 10px; font-size: 12px; max-height: 340px; overflow: auto; }
376
+ .tool-sec + .tool-sec { margin-top: 8px; }
377
+ .tool-sec b { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
378
+ .tool-sec.muted { color: var(--muted); font-style: italic; }
379
+ .tool-body pre { margin: 3px 0 0; white-space: pre-wrap; word-break: break-word; font-size: 11.5px; line-height: 1.45; }
297
380
  .plan { margin: 4px 0 8px; font-size: 12px; font-weight: 600; background: #fff; border-radius: 10px; padding: 8px 10px; }
298
381
  .plan-entry.completed { text-decoration: line-through; color: var(--muted); }
299
382
  .plan-entry.in_progress { font-weight: 800; }
@@ -301,10 +384,12 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
301
384
  .meta .i { width: 12px; height: 12px; }
302
385
  .msg.mine .meta { justify-content: flex-end; }
303
386
  .head .edited { font-size: 10.5px; color: var(--faint); font-style: italic; margin-left: 6px; cursor: help; }
304
- .head .edit-btn { border: 0; background: none; color: var(--faint); cursor: pointer; padding: 0 4px; opacity: 0; transition: opacity var(--t-fast), color var(--t-fast); border-radius: 6px; line-height: 0; }
305
- .head .edit-btn .i { width: 14px; height: 14px; }
306
- .msg:hover .head .edit-btn, .head .edit-btn:focus { opacity: 1; }
387
+ .head .edit-btn, .head .pin-btn { border: 0; background: none; color: var(--faint); cursor: pointer; padding: 0 4px; opacity: 0; transition: opacity var(--t-fast), color var(--t-fast); border-radius: 6px; line-height: 0; }
388
+ .head .edit-btn .i, .head .pin-btn .i { width: 14px; height: 14px; }
389
+ .msg:hover .head .edit-btn, .head .edit-btn:focus, .msg:hover .head .pin-btn, .head .pin-btn:focus { opacity: 1; }
307
390
  .head .edit-btn:hover { color: var(--primary); }
391
+ .head .pin-btn:hover { color: #ff8a3d; }
392
+ .msg.pinned .head .pin-btn { opacity: 1; color: #ff8a3d; }
308
393
  .edit-box { margin: 4px 0 6px; }
309
394
  .msg .bubble-col:has(> .bubble > .edit-box:not([hidden])) { width: 100%; }
310
395
  .msg .bubble:has(> .edit-box:not([hidden])) { width: min(760px, 100%); }
@@ -347,20 +432,44 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
347
432
  .side-list li.offline .p-name, .side-list li.offline .p-sub { color: var(--muted); }
348
433
  .side-list li.me .avatar .av-tile, .msg.mine .avatar .av-tile { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--primary); }
349
434
  .msg.mine .avatar .av-tile { box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px var(--primary); }
350
- .composer { display: flex; gap: 10px; padding: 8px 8px 8px 18px; margin: 10px 16px; background: var(--softer); border-radius: 20px; align-items: center; min-height: 60px; transition: box-shadow var(--t-fast); }
435
+ .composer { position: relative; display: flex; gap: 10px; padding: 8px 8px 8px 18px; margin: 10px 16px; background: var(--softer); border-radius: 20px; align-items: center; min-height: 60px; transition: box-shadow var(--t-fast); }
436
+ .composer-grip { position: absolute; left: 24px; right: 24px; top: -6px; height: 12px; cursor: ns-resize; z-index: 3; touch-action: none; }
437
+ .composer-grip::after { content: ""; position: absolute; left: 50%; top: 4px; width: 44px; height: 4px; margin-left: -22px; border-radius: 2px; background: var(--lav-2); opacity: 0; transition: opacity var(--t-fast), background var(--t-fast); }
438
+ .composer-grip:hover::after, .composer.resizing .composer-grip::after { opacity: 1; background: var(--primary); }
439
+ .composer.resizing { user-select: none; }
351
440
  .composer:focus-within { box-shadow: 0 0 0 2px var(--primary); }
352
441
  .composer .smile-btn { border: 0; background: transparent; color: var(--placeholder); padding: 0; display: grid; place-content: center; width: 28px; height: 28px; border-radius: 8px; flex: none; }
353
442
  .composer .smile-btn .i { width: 20px; height: 20px; }
354
443
  .composer .smile-btn:hover { color: var(--primary); }
355
444
  .composer-box { flex: 1; min-width: 0; display: flex; align-items: center; }
356
- .composer textarea { flex: 1; display: block; box-sizing: border-box; resize: none; padding: 8px 0; margin: 0; border: 0; border-radius: 0; background: transparent; height: 36px; min-height: 36px; max-height: 180px; line-height: 20px; font-size: 14px; font-weight: 600; outline: none; color: var(--ink); overflow-y: auto; }
445
+ .composer-box:has(.shots-tray:not([hidden])) { flex-direction: column; align-items: stretch; }
446
+ .composer-box:has(.shots-tray:not([hidden])) textarea { flex: none; }
447
+ .composer.drop-target { box-shadow: 0 0 0 2px var(--primary); }
448
+ .shots-tray { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0 2px; }
449
+ .shot-chip { position: relative; display: block; width: 56px; height: 56px; border-radius: 10px; overflow: hidden; background: #fff; box-shadow: 0 1px 3px rgba(28, 27, 51, 0.16); }
450
+ .shot-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
451
+ .shot-drop { position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; border: 0; border-radius: 50%; background: rgba(28, 27, 51, 0.72); color: #fff; font-size: 13px; line-height: 1; padding: 0; cursor: pointer; }
452
+ .shot-drop:hover { background: var(--primary); }
453
+ .shot-n { position: absolute; top: 3px; left: 3px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 6px; background: rgba(28, 27, 51, 0.72); color: #fff; font-size: 11px; font-weight: 800; line-height: 18px; text-align: center; pointer-events: none; }
454
+ .bubble .shots { display: flex; flex-wrap: wrap; gap: 8px; }
455
+ .bubble .shots:not([hidden]) { margin-top: 8px; }
456
+ .bubble .shot { position: relative; display: block; width: 72px; height: 72px; padding: 0; border: 0; border-radius: 12px; overflow: hidden; line-height: 0; background: #fff; box-shadow: 0 1px 3px rgba(28, 27, 51, 0.16); cursor: zoom-in; transition: transform var(--t-fast) var(--ease-out); }
457
+ .bubble .shot:hover { transform: translateY(-1px) scale(1.03); }
458
+ .bubble .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
459
+ .img-ref { display: inline-flex; align-items: center; gap: 3px; vertical-align: -3px; height: 20px; padding: 0 6px 0 4px; margin: 0 1px; border: 0; border-radius: 6px; background: rgba(28, 27, 51, 0.1); color: inherit; font: inherit; font-size: 11px; font-weight: 800; line-height: 20px; cursor: zoom-in; }
460
+ .msg.mine .img-ref { background: rgba(255, 255, 255, 0.24); }
461
+ .img-ref svg { width: 14px; height: 14px; }
462
+ .img-ref:hover { background: var(--primary); color: #fff; }
463
+ .lightbox { position: fixed; inset: 0; z-index: 200; display: grid; place-content: center; padding: 24px; background: rgba(28, 27, 51, 0.72); cursor: zoom-out; animation: fade-in var(--t-base) var(--ease-out); }
464
+ .lightbox img { max-width: calc(100vw - 48px); max-height: calc(100vh - 48px); border-radius: 12px; box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6); background: #fff; }
465
+ .composer textarea { flex: 1; display: block; box-sizing: border-box; resize: none; padding: 8px 0; margin: 0; border: 0; border-radius: 0; background: transparent; height: 36px; min-height: 36px; line-height: 20px; font-size: 14px; font-weight: 600; outline: none; color: var(--ink); overflow-y: auto; }
357
466
  .composer textarea::placeholder { color: var(--placeholder); font-weight: 600; }
358
467
  .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); }
359
468
  .send-btn .i { width: 18px; height: 18px; display: block; }
360
469
  .send-btn:hover { filter: brightness(1.05); transform: translateY(-1px); }
361
470
  .send-btn:active { transform: translateY(1px); }
362
- .chat .mention-menu { left: 24px; bottom: 78px; }
363
- .chat .emoji-menu { left: 24px; bottom: 78px; }
471
+ .chat .mention-menu { left: 24px; bottom: calc(var(--composer-h, 60px) + 18px); }
472
+ .chat .emoji-menu { left: 24px; bottom: calc(var(--composer-h, 60px) + 18px); }
364
473
  .page-inner { flex: 1; overflow-y: auto; padding: 8px 22px 22px; }
365
474
  .page-inner > .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; min-height: 48px; }
366
475
  .page-inner h1 { font-size: 20px; letter-spacing: -0.01em; }
@@ -375,9 +484,6 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
375
484
  .resizer { position: absolute; left: -14px; top: 0; bottom: 0; width: 20px; cursor: col-resize; z-index: 3; }
376
485
  .resizer::after { content: ""; position: absolute; left: 8.5px; top: 44%; height: 12%; width: 3px; border-radius: 3px; background: var(--lav-2); opacity: 0; transition: opacity var(--t-fast); }
377
486
  .resizer:hover::after, .shell.resizing .resizer::after { opacity: 1; }
378
- .details-handle { position: absolute; left: 0; bottom: 18%; z-index: 4; width: 20px; height: 64px; padding: 0; border: 0; border-radius: 0 12px 12px 0; background: #d6d8fb; color: var(--primary); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 8px -2px rgba(91, 91, 240, 0.45); transition: background var(--t-fast), color var(--t-fast); }
379
- .details-handle:hover { background: var(--primary); color: #fff; }
380
- .details-handle svg { width: 16px; height: 16px; }
381
487
  .panel-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; }
382
488
  .panel-title h3 { font-size: 11px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
383
489
  .panel-title .hint { display: none; }
@@ -427,3 +533,42 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
427
533
  .details { position: fixed; right: var(--gap); top: var(--gap); bottom: var(--gap); z-index: 20; width: min(var(--details-w), 92vw); }
428
534
  .chat-actions .search { width: 160px; }
429
535
  }
536
+ .tpl-layout { display: grid; grid-template-columns: 200px 1fr; gap: 14px; margin: 6px 0 12px; }
537
+ .tpl-list { display: flex; flex-direction: column; gap: 8px; }
538
+ .tpl-list::before { content: "Templates"; font-size: 11px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--faint); padding: 0 4px; }
539
+ .tpl-item { display: grid; grid-template-columns: 34px 1fr 18px; align-items: center; gap: 10px; text-align: left; border: 2px solid transparent; border-radius: 14px; padding: 10px 10px 10px 12px; background: var(--card); color: var(--ink); font: inherit; cursor: pointer; box-shadow: 0 1px 3px rgba(28, 27, 51, 0.06); transition: border-color var(--t-fast), transform var(--t-fast); }
540
+ .tpl-item:hover { border-color: var(--lav-2); transform: translateY(-1px); }
541
+ .tpl-item.on { border-color: var(--primary); background: var(--lav); }
542
+ .tpl-emoji { font-size: 22px; line-height: 1; text-align: center; }
543
+ .tpl-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
544
+ .tpl-body b { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
545
+ .tpl-meta { color: var(--muted); font-size: 12px; font-weight: 600; }
546
+ .tpl-item .avatar-stack { margin-top: 2px; }
547
+ .tpl-rec { background: var(--warm); color: var(--warm-ink); font-size: 10px; padding: 1px 7px; }
548
+ .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); }
549
+ .tpl-check .i { width: 12px; height: 12px; }
550
+ .tpl-item.on .tpl-check { opacity: 1; }
551
+ .tpl-detail { min-width: 0; display: flex; flex-direction: column; gap: 10px; max-height: 46vh; overflow: auto; padding-right: 4px; }
552
+ .tpl-desc { margin: 0; color: var(--ink-2); font-weight: 600; }
553
+ .tpl-rules ul { margin: 4px 0 0; padding-left: 18px; color: var(--ink-2); font-size: 13px; font-weight: 600; }
554
+ .tpl-vm { border-radius: 14px; background: var(--soft); padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
555
+ .tpl-vm-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
556
+ .tpl-vm-role { color: var(--muted); font-size: 12.5px; font-weight: 600; line-height: 1.45; }
557
+ .mm-all { width: 24px; height: 24px; border-radius: 8px; background: var(--lav); color: var(--primary); display: grid; place-content: center; flex: none; }
558
+ .mm-all .i { width: 14px; height: 14px; }
559
+ .mention.all { color: var(--primary); }
560
+ .side-list li.unstaffed .avatar .av-tile { filter: grayscale(0.8); opacity: 0.55; }
561
+ .side-list li.unstaffed .p-name > span:first-child { color: var(--muted); }
562
+ .badge.status-unstaffed { background: var(--warm); color: var(--warm-ink); cursor: pointer; }
563
+ .cast-banner { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 18px 22px 14px; background: var(--grad-canvas); border-bottom: 1px solid var(--lav); text-align: center; }
564
+ .cast-lead { max-width: 720px; color: var(--ink-2); font-weight: 600; font-size: 14px; }
565
+ .cast-list { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 12px; }
566
+ .cast-card { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 200px; padding: 16px 12px; border: 2px dashed var(--lav-2); border-radius: 16px; background: var(--card); color: var(--ink); font: inherit; cursor: pointer; transition: border-color var(--t-fast), transform var(--t-fast); }
567
+ .cast-card:hover { border-color: var(--primary); transform: translateY(-2px); }
568
+ .cast-card .av-tile { filter: grayscale(0.8); opacity: 0.7; }
569
+ .cast-card b { font-size: 15px; }
570
+ .cast-card span { color: var(--muted); font-size: 12px; font-weight: 600; }
571
+ .cast-card em { margin-top: 4px; font-style: normal; font-size: 12px; font-weight: 800; color: var(--primary); }
572
+ .composer.gated { opacity: 0.7; }
573
+ .composer.gated textarea { cursor: not-allowed; }
574
+ .check-list.locked { pointer-events: none; opacity: 0.6; }