viberoom 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1",
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. 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
+ }
@@ -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,34 @@
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: center; gap: 8px; max-width: 230px; padding: 6px 12px 6px 8px; border: 0; border-radius: 14px; background: var(--warm); color: var(--warm-ink); font: inherit; text-align: left; cursor: pointer; box-shadow: var(--shadow-pop); animation: rise var(--t-base) var(--ease-out); overflow: hidden; }
268
+ .done-note:hover { background: #ffeaa8; }
269
+ .done-note .avatar { flex: none; }
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: #ffeaa8; }
272
+ .done-go .avatar { flex: none; }
273
+ .done-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
274
+ .done-text b { font-size: 12px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
275
+ .done-text small { font-size: 11px; font-weight: 600; opacity: 0.8; white-space: nowrap; }
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: #ffeaa8; }
278
+ .jump-latest .i { width: 16px; height: 16px; }
209
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); }
210
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; }
211
281
  .msg.mine .bubble .text { color: #fff; }
282
+ .msg.mine.waiting .bubble { background: linear-gradient(135deg, #ff9447, #e8642a); box-shadow: 0 8px 18px -10px rgba(232, 100, 42, 0.7); }
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
+ .bubble .waiting .i { width: 14px; height: 14px; vertical-align: -2px; }
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: #c9461c; font-weight: 800; font-size: 12.5px; cursor: pointer; }
287
+ .bubble .waiting-stop:hover:not(:disabled) { background: #fff; }
288
+ .bubble .waiting-stop:disabled { opacity: 0.7; cursor: default; }
212
289
  .msg.mine .mention { background: rgba(255, 255, 255, 0.22); color: #fff !important; padding: 1px 7px; border-radius: 6px; font-weight: 800; }
213
290
  .msg.mine .bubble code { background: rgba(255, 255, 255, 0.18); color: #fff; }
214
291
  .msg.mine .open-link { color: #fff; text-decoration-color: rgba(255, 255, 255, 0.5); }
215
292
  .bubble .head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 2px; }
216
- .head { display: flex; gap: 8px; align-items: baseline; padding: 0 4px; }
293
+ .head { display: flex; gap: 8px; align-items: center; padding: 0 4px; }
217
294
  .head .name { font-weight: 800; font-size: 14px; }
218
295
  .head .time { color: var(--faint); font-size: 11px; font-weight: 600; cursor: default; white-space: nowrap; }
219
296
  .msg.mine .head { justify-content: flex-end; }
@@ -258,7 +335,7 @@
258
335
  .tn.selected { background: var(--lav-2); color: var(--primary); }
259
336
  .tn.hidden-dir { color: var(--muted); font-weight: 600; }
260
337
  .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; }
338
+ .tn-tw .i { width: 14px; height: 14px; }
262
339
  .tn-tw:hover { background: var(--card); color: var(--primary); }
263
340
  .tn.open .tn-tw { transform: rotate(90deg); }
264
341
  .tn.leaf .tn-tw { visibility: hidden; }
@@ -294,6 +371,14 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
294
371
  .agent-notices:empty, .tools:empty, .meta:empty, .perms:empty { display: none; }
295
372
  .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
373
  .tools { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
374
+ .tool { display: inline-flex; flex-direction: column; max-width: 100%; min-width: 0; }
375
+ .tool.open { flex-basis: 100%; }
376
+ .tool > .chip { font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; text-align: left; }
377
+ .tool-body { margin: 4px 0 2px; padding: 8px 10px; background: var(--soft); border-radius: 10px; font-size: 12px; max-height: 340px; overflow: auto; }
378
+ .tool-sec + .tool-sec { margin-top: 8px; }
379
+ .tool-sec b { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
380
+ .tool-sec.muted { color: var(--muted); font-style: italic; }
381
+ .tool-body pre { margin: 3px 0 0; white-space: pre-wrap; word-break: break-word; font-size: 11.5px; line-height: 1.45; }
297
382
  .plan { margin: 4px 0 8px; font-size: 12px; font-weight: 600; background: #fff; border-radius: 10px; padding: 8px 10px; }
298
383
  .plan-entry.completed { text-decoration: line-through; color: var(--muted); }
299
384
  .plan-entry.in_progress { font-weight: 800; }
@@ -301,10 +386,12 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
301
386
  .meta .i { width: 12px; height: 12px; }
302
387
  .msg.mine .meta { justify-content: flex-end; }
303
388
  .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; }
389
+ .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; }
390
+ .head .edit-btn .i, .head .pin-btn .i { width: 14px; height: 14px; }
391
+ .msg:hover .head .edit-btn, .head .edit-btn:focus, .msg:hover .head .pin-btn, .head .pin-btn:focus { opacity: 1; }
307
392
  .head .edit-btn:hover { color: var(--primary); }
393
+ .head .pin-btn:hover { color: #ff8a3d; }
394
+ .msg.pinned .head .pin-btn { opacity: 1; color: #ff8a3d; }
308
395
  .edit-box { margin: 4px 0 6px; }
309
396
  .msg .bubble-col:has(> .bubble > .edit-box:not([hidden])) { width: 100%; }
310
397
  .msg .bubble:has(> .edit-box:not([hidden])) { width: min(760px, 100%); }
@@ -347,20 +434,46 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
347
434
  .side-list li.offline .p-name, .side-list li.offline .p-sub { color: var(--muted); }
348
435
  .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
436
  .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); }
437
+ .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); }
438
+ .composer-grip { position: absolute; left: 24px; right: 24px; top: -6px; height: 12px; cursor: ns-resize; z-index: 3; touch-action: none; }
439
+ .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); }
440
+ .composer-grip:hover::after, .composer.resizing .composer-grip::after { opacity: 1; background: var(--primary); }
441
+ .composer.resizing { user-select: none; }
351
442
  .composer:focus-within { box-shadow: 0 0 0 2px var(--primary); }
352
443
  .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
444
  .composer .smile-btn .i { width: 20px; height: 20px; }
354
445
  .composer .smile-btn:hover { color: var(--primary); }
355
446
  .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; }
447
+ .composer-box:has(.shots-tray:not([hidden])) { flex-direction: column; align-items: stretch; }
448
+ .composer-box:has(.shots-tray:not([hidden])) textarea { flex: none; }
449
+ .composer.drop-target { box-shadow: 0 0 0 2px var(--primary); }
450
+ .shots-tray { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0 2px; }
451
+ .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); }
452
+ .shot-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
453
+ .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; }
454
+ .shot-drop:hover { background: var(--primary); }
455
+ .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; }
456
+ .bubble .shots { display: flex; flex-wrap: wrap; gap: 8px; }
457
+ .bubble .shots:not([hidden]) { margin-top: 8px; }
458
+ .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); }
459
+ .bubble .shot:hover { transform: translateY(-1px) scale(1.03); }
460
+ .bubble .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
461
+ .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; }
462
+ .msg.mine .img-ref { background: rgba(255, 255, 255, 0.24); }
463
+ .img-ref svg { width: 14px; height: 14px; }
464
+ .img-ref:hover { background: var(--primary); color: #fff; }
465
+ .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); }
466
+ .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; }
467
+ .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
468
  .composer textarea::placeholder { color: var(--placeholder); font-weight: 600; }
358
469
  .send-btn { width: 44px; height: 44px; border-radius: 14px; border: 0; background: var(--grad-primary); color: #fff; display: grid; place-content: center; box-shadow: var(--shadow-primary); flex: none; transition: transform var(--t-fast) var(--ease-out), filter var(--t-fast); }
359
470
  .send-btn .i { width: 18px; height: 18px; display: block; }
471
+ .composer-clear { width: 28px; height: 28px; border-radius: 50%; border: 0; background: transparent; color: var(--placeholder); font-size: 20px; line-height: 1; cursor: pointer; flex: none; margin-right: 2px; transition: background var(--t-fast), color var(--t-fast); }
472
+ .composer-clear:hover { background: var(--soft); color: var(--danger); }
360
473
  .send-btn:hover { filter: brightness(1.05); transform: translateY(-1px); }
361
474
  .send-btn:active { transform: translateY(1px); }
362
- .chat .mention-menu { left: 24px; bottom: 78px; }
363
- .chat .emoji-menu { left: 24px; bottom: 78px; }
475
+ .chat .mention-menu { left: 24px; bottom: calc(var(--composer-h, 60px) + 18px); }
476
+ .chat .emoji-menu { left: 24px; bottom: calc(var(--composer-h, 60px) + 18px); }
364
477
  .page-inner { flex: 1; overflow-y: auto; padding: 8px 22px 22px; }
365
478
  .page-inner > .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; min-height: 48px; }
366
479
  .page-inner h1 { font-size: 20px; letter-spacing: -0.01em; }
@@ -375,9 +488,6 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
375
488
  .resizer { position: absolute; left: -14px; top: 0; bottom: 0; width: 20px; cursor: col-resize; z-index: 3; }
376
489
  .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
490
  .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
491
  .panel-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; }
382
492
  .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
493
  .panel-title .hint { display: none; }
@@ -427,3 +537,48 @@ table.csv tbody tr:nth-child(even) td { background: rgba(91, 91, 240, 0.03); }
427
537
  .details { position: fixed; right: var(--gap); top: var(--gap); bottom: var(--gap); z-index: 20; width: min(var(--details-w), 92vw); }
428
538
  .chat-actions .search { width: 160px; }
429
539
  }
540
+ .tpl-layout { display: grid; grid-template-columns: 200px 1fr; gap: 14px; margin: 6px 0 12px; }
541
+ .tpl-list { display: flex; flex-direction: column; gap: 8px; }
542
+ .tpl-list::before { content: "Templates"; font-size: 11px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--faint); padding: 0 4px; }
543
+ .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); }
544
+ .tpl-item:hover { border-color: var(--lav-2); transform: translateY(-1px); }
545
+ .tpl-item.on { border-color: var(--primary); background: var(--lav); }
546
+ .tpl-emoji { font-size: 22px; line-height: 1; text-align: center; }
547
+ .tpl-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
548
+ .tpl-body b { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
549
+ .tpl-meta { color: var(--muted); font-size: 12px; font-weight: 600; }
550
+ .tpl-item .avatar-stack { margin-top: 2px; }
551
+ .tpl-rec { background: var(--warm); color: var(--warm-ink); font-size: 10px; padding: 1px 7px; }
552
+ .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); }
553
+ .tpl-check .i { width: 12px; height: 12px; }
554
+ .tpl-item.on .tpl-check { opacity: 1; }
555
+ .tpl-detail { min-width: 0; display: flex; flex-direction: column; gap: 10px; max-height: 46vh; overflow: auto; padding-right: 4px; }
556
+ .tpl-desc { margin: 0; color: var(--ink-2); font-weight: 600; }
557
+ .tpl-rules ul { margin: 4px 0 0; padding-left: 18px; color: var(--ink-2); font-size: 13px; font-weight: 600; }
558
+ .tpl-vm { border-radius: 14px; background: var(--soft); padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
559
+ .tpl-vm-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
560
+ .tpl-vm-role { color: var(--muted); font-size: 12.5px; font-weight: 600; line-height: 1.45; }
561
+ .mm-all { width: 24px; height: 24px; border-radius: 8px; background: var(--lav); color: var(--primary); display: grid; place-content: center; flex: none; }
562
+ .mm-all .i { width: 14px; height: 14px; }
563
+ .mention.all { color: var(--primary); }
564
+ .side-list li.unstaffed .avatar .av-tile { filter: grayscale(0.8); opacity: 0.55; }
565
+ .side-list li.unstaffed .p-name > span:first-child { color: var(--muted); }
566
+ .badge.status-unstaffed { background: var(--warm); color: var(--warm-ink); cursor: pointer; }
567
+ .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; }
568
+ .cast-lead { max-width: 720px; color: var(--ink-2); font-weight: 600; font-size: 14px; }
569
+ .cast-list { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 12px; }
570
+ .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); }
571
+ .cast-card:hover { border-color: var(--primary); transform: translateY(-2px); }
572
+ .cast-card .av-tile { filter: grayscale(0.8); opacity: 0.7; }
573
+ .cast-card b { font-size: 15px; }
574
+ .cast-card span { color: var(--muted); font-size: 12px; font-weight: 600; }
575
+ .cast-card em { margin-top: 4px; font-style: normal; font-size: 12px; font-weight: 800; color: var(--primary); }
576
+ .composer.gated { opacity: 0.7; }
577
+ .composer.gated textarea { cursor: not-allowed; }
578
+ .check-list.locked { pointer-events: none; opacity: 0.6; }
579
+ .msg.system.done { justify-content: center; }
580
+ .done-row { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px 6px 8px; border: 0; border-radius: var(--r-pill); background: var(--warm); color: var(--warm-ink); font: inherit; font-size: 12px; font-weight: 800; cursor: pointer; box-shadow: var(--edge); }
581
+ .done-row:hover { background: #ffeaa8; }
582
+ .done-notes { flex-direction: column; }
583
+ .done-note.new { background: var(--card); color: var(--ink-2); }
584
+ .done-note.new:hover { background: var(--soft); }