viberoom 0.7.0 → 0.8.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.
- package/README.md +11 -0
- package/dist/hub.js +94 -48
- package/dist/look-lint.js +180 -0
- package/dist/looks.js +151 -0
- package/dist/mcp-skills-server.js +87 -0
- package/dist/persona.js +2 -0
- package/dist/room.js +134 -4
- package/dist/server.js +70 -0
- package/dist/skills.js +23 -2
- package/package.json +1 -1
- package/ui/app.css +20 -16
- package/ui/app.js +107 -33
- package/ui/components.css +96 -115
- package/ui/components.js +8 -3
- package/ui/index.html +1 -0
- package/ui/looks.css +462 -77
- package/ui/theme.css +90 -13
- package/ui/tokens.js +526 -57
package/dist/persona.js
CHANGED
|
@@ -10,6 +10,7 @@ export function skillPull(reply) {
|
|
|
10
10
|
}
|
|
11
11
|
export const SKILL_WRITER_NAME = "skill-writer";
|
|
12
12
|
export const ROOM_DESIGNER_NAME = "room-designer";
|
|
13
|
+
export const LOOK_DESIGNER_NAME = "look-designer";
|
|
13
14
|
export const NAME_PATTERN = /^[\p{L}\p{N}][\p{L}\p{N}_-]{0,23}$/u;
|
|
14
15
|
export const ROOM_SETTINGS_SPEC = {
|
|
15
16
|
name: { kind: "own-path", brief: true, agent: false, doc: "The room's name; changed with rename." },
|
|
@@ -247,6 +248,7 @@ function skillsSection(skills) {
|
|
|
247
248
|
if (skills.canCreate) {
|
|
248
249
|
lines.push(`You may also create skills for the shared library when a procedure is worth reusing (by you later, or by other agents): first load the built-in skill "${SKILL_WRITER_NAME}" with the viberoom ${SKILL_TOOL_NAME} tool for the rules of a good skill, then call the viberoom tools create_skill (name, description, instructions) and attach_skill to give it to yourself or to other agents. These are MCP tools of the "viberoom" server, not your own skill commands. The human sees every new skill in Settings.`);
|
|
249
250
|
lines.push(`You may also design rooms: load the built-in skill "${ROOM_DESIGNER_NAME}" first, then describe_room for the facts, lint_room_design to check a draft (it previews the brief the vibemates would read), create_template to save a template the human can pick under New room, and propose_room_changes to suggest a change to this room: it becomes a card the human applies or rejects, so nothing here changes without their click.`);
|
|
251
|
+
lines.push(`You may also design looks (how the human's window is drawn: colours, shadows, corners, fonts): load the built-in skill "${LOOK_DESIGNER_NAME}" first, then describe_looks for the facts, lint_look to check a draft (it measures whether the words read), create_look to save a look the human can pick under Settings, and propose_look_changes to suggest wearing a look or fine-tuning one: a card the human applies or rejects.`);
|
|
250
252
|
}
|
|
251
253
|
else if (skills.items.length) {
|
|
252
254
|
lines.push("Skills are created by the human or by agents that have the hub's tools; if you want a new one, describe it in the room.");
|
package/dist/room.js
CHANGED
|
@@ -1205,6 +1205,11 @@ export class Room extends EventEmitter {
|
|
|
1205
1205
|
const skill = this.skills?.library.get(message.skill.name);
|
|
1206
1206
|
if (!message.to.length)
|
|
1207
1207
|
targets = targets.filter((id) => this.hasSkill(this.participants.get(id), message.skill.name));
|
|
1208
|
+
if (!targets.length && !message.to.length) {
|
|
1209
|
+
const alone = [...this.runtimes.keys()].filter(live);
|
|
1210
|
+
if (alone.length === 1)
|
|
1211
|
+
targets = alone;
|
|
1212
|
+
}
|
|
1208
1213
|
if (!targets.length)
|
|
1209
1214
|
this.notice(`Nobody in this room has the skill "${message.skill.name}"; attach it to an agent first, or address one with @.`, "warn");
|
|
1210
1215
|
if (skill) {
|
|
@@ -1241,13 +1246,19 @@ export class Room extends EventEmitter {
|
|
|
1241
1246
|
this.requestTurn(id, message.to.includes(id) || !!message.skill);
|
|
1242
1247
|
}
|
|
1243
1248
|
hasSkill(participant, name) {
|
|
1249
|
+
if (this.isBuiltinSkill(name))
|
|
1250
|
+
return true;
|
|
1244
1251
|
if (!participant?.skills)
|
|
1245
1252
|
return false;
|
|
1246
1253
|
const lower = name.toLowerCase();
|
|
1247
1254
|
return participant.skills.some((s) => s.toLowerCase() === lower);
|
|
1248
1255
|
}
|
|
1256
|
+
isBuiltinSkill(name) {
|
|
1257
|
+
const skill = this.skills?.library.get(name);
|
|
1258
|
+
return !!skill && skill.author === BUILTIN_AUTHOR && !skill.draft && !skill.problems.length;
|
|
1259
|
+
}
|
|
1249
1260
|
attachedSkills(participant) {
|
|
1250
|
-
if (!this.skills
|
|
1261
|
+
if (!this.skills)
|
|
1251
1262
|
return [];
|
|
1252
1263
|
return this.skills.library.list().filter((s) => !s.problems.length && !s.draft && this.hasSkill(participant, s.name));
|
|
1253
1264
|
}
|
|
@@ -1443,6 +1454,116 @@ export class Room extends EventEmitter {
|
|
|
1443
1454
|
warnings: proposal.warnings,
|
|
1444
1455
|
};
|
|
1445
1456
|
}
|
|
1457
|
+
async describeLooksForAgent(participantId) {
|
|
1458
|
+
const participant = this.agentInRoom(participantId);
|
|
1459
|
+
if (!this.skills?.looks || !this.skills.appearance)
|
|
1460
|
+
throw new Error("looks are not available in this hub");
|
|
1461
|
+
const described = await this.skills.looks.describe();
|
|
1462
|
+
return { you: participant.name, human: this.settings.humanName, appearance: this.skills.appearance.current(), ...described };
|
|
1463
|
+
}
|
|
1464
|
+
async lintLookForAgent(participantId, raw) {
|
|
1465
|
+
this.agentInRoom(participantId);
|
|
1466
|
+
if (!this.skills?.looks)
|
|
1467
|
+
throw new Error("looks are not available in this hub");
|
|
1468
|
+
try {
|
|
1469
|
+
const checked = await this.skills.looks.check(raw);
|
|
1470
|
+
return { ok: checked.lint.ok, id: checked.spec.id, errors: checked.lint.errors.map((e) => ({ key: e.key, message: e.message })), warnings: checked.lint.warnings.map((w) => ({ key: w.key, message: w.message })), report: checked.lint.report };
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
return { ok: false, errors: [{ key: "spec", message: error instanceof Error ? error.message : String(error) }], warnings: [], report: [] };
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async createLookForAgent(participantId, raw, replace) {
|
|
1477
|
+
const participant = this.agentInRoom(participantId);
|
|
1478
|
+
if (!this.skills?.looks)
|
|
1479
|
+
throw new Error("looks are not available in this hub");
|
|
1480
|
+
let saved;
|
|
1481
|
+
try {
|
|
1482
|
+
saved = await this.skills.looks.save(raw, { author: participant.name, replace });
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
throw new Error(`not saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
1486
|
+
}
|
|
1487
|
+
const warnings = saved.lint.warnings.map((w) => w.message);
|
|
1488
|
+
this.postSystem(`${participant.name} saved the look "${saved.spec.label}" (Settings → Appearance).`);
|
|
1489
|
+
this.log.info(`${participant.name} saved look ${saved.spec.id}`);
|
|
1490
|
+
return {
|
|
1491
|
+
ok: true,
|
|
1492
|
+
message: `Saved the look "${saved.spec.label}" (id ${saved.spec.id}) among ${this.settings.humanName}'s own looks: it is in Settings → Appearance now, after the looks viberoom ships. Nothing is worn until ${this.settings.humanName} picks it; propose_look_changes with look "${saved.spec.id}" offers it as a card.${warnings.length ? ` Warnings: ${warnings.join("; ")}` : ""}`,
|
|
1493
|
+
id: saved.spec.id,
|
|
1494
|
+
warnings,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
async proposeLookChanges(participantId, why, changes) {
|
|
1498
|
+
const participant = this.agentInRoom(participantId);
|
|
1499
|
+
if (!this.skills?.appearance || !this.skills.looks)
|
|
1500
|
+
throw new Error("looks are not available in this hub");
|
|
1501
|
+
const current = this.skills.appearance.current();
|
|
1502
|
+
const patch = {};
|
|
1503
|
+
const rows = [];
|
|
1504
|
+
if (changes.look !== undefined) {
|
|
1505
|
+
const lookId = String(changes.look).trim();
|
|
1506
|
+
const known = await this.skills.appearance.ownAdjustments(lookId);
|
|
1507
|
+
if (!known)
|
|
1508
|
+
throw new Error(`not proposed: no look "${lookId}" (the looks viberoom ships, or one of ${this.settings.humanName}'s own by its id)`);
|
|
1509
|
+
patch.look = lookId;
|
|
1510
|
+
if (lookId !== current.look)
|
|
1511
|
+
rows.push({ key: "look", from: current.look, to: lookId });
|
|
1512
|
+
}
|
|
1513
|
+
if (changes.adjust !== undefined) {
|
|
1514
|
+
if (!changes.adjust || typeof changes.adjust !== "object" || Array.isArray(changes.adjust))
|
|
1515
|
+
throw new Error("not proposed: adjust is an object of adjustable keys and values");
|
|
1516
|
+
const lookId = String(changes.look ?? current.look);
|
|
1517
|
+
const own = await this.skills.appearance.ownAdjustments(lookId);
|
|
1518
|
+
if (!own)
|
|
1519
|
+
throw new Error(`not proposed: no look "${lookId}" to fine-tune`);
|
|
1520
|
+
const merged = { ...(current.custom?.[lookId] ?? {}), ...changes.adjust };
|
|
1521
|
+
patch.custom = { [lookId]: merged };
|
|
1522
|
+
const previewed = this.skills.appearance.preview({ custom: { [lookId]: merged } });
|
|
1523
|
+
for (const key of Object.keys(changes.adjust)) {
|
|
1524
|
+
const from = current.custom?.[lookId]?.[key] ?? own.values[key] ?? "";
|
|
1525
|
+
const to = previewed.custom[lookId]?.[key] ?? "";
|
|
1526
|
+
if (String(from).toLowerCase() !== String(to).toLowerCase())
|
|
1527
|
+
rows.push({ key: `${key} (${own.label})`, from, to });
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
for (const key of ["chatFontSize", "font", "mono"]) {
|
|
1531
|
+
if (changes[key] === undefined)
|
|
1532
|
+
continue;
|
|
1533
|
+
patch[key] = changes[key];
|
|
1534
|
+
const previewed = this.skills.appearance.preview({ [key]: changes[key] });
|
|
1535
|
+
if (String(previewed[key]) !== String(current[key]))
|
|
1536
|
+
rows.push({ key: key === "chatFontSize" ? "text size" : key === "font" ? "font" : "code font", from: current[key], to: previewed[key] });
|
|
1537
|
+
}
|
|
1538
|
+
this.skills.appearance.preview(patch);
|
|
1539
|
+
if (!rows.length)
|
|
1540
|
+
throw new Error("not proposed: the change leaves the window as it is");
|
|
1541
|
+
const proposal = {
|
|
1542
|
+
key: randomUUID(),
|
|
1543
|
+
participantId,
|
|
1544
|
+
participantName: participant.name,
|
|
1545
|
+
ts: Date.now(),
|
|
1546
|
+
why: String(why ?? "").trim().slice(0, 600),
|
|
1547
|
+
settings: [],
|
|
1548
|
+
vibemates: [],
|
|
1549
|
+
appearance: rows,
|
|
1550
|
+
warnings: [],
|
|
1551
|
+
touchesOwn: false,
|
|
1552
|
+
status: "pending",
|
|
1553
|
+
};
|
|
1554
|
+
this.proposalPlans.set(proposal.key, { settings: [], vibemates: [], ops: [], ids: {}, appearance: patch });
|
|
1555
|
+
this.proposals.set(proposal.key, proposal);
|
|
1556
|
+
this.push({ type: "proposal", proposal });
|
|
1557
|
+
const what = rows.map((c) => c.key).join(", ");
|
|
1558
|
+
this.postSystem(`${participant.name} proposes a change to how the window looks (${what}); apply or reject it on the card.`, "human", false, { tone: "attention" });
|
|
1559
|
+
this.log.info(`look proposal ${proposal.key} from ${participant.name}: ${what}`);
|
|
1560
|
+
return {
|
|
1561
|
+
ok: true,
|
|
1562
|
+
message: `Proposal sent to ${this.settings.humanName} as a card in the room (${what}). It changes the whole window, not this room alone; nothing changes until they apply it, and you will see a room line with the outcome.`,
|
|
1563
|
+
key: proposal.key,
|
|
1564
|
+
warnings: [],
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1446
1567
|
async resolveProposal(key, accept) {
|
|
1447
1568
|
const proposal = this.proposals.get(key);
|
|
1448
1569
|
const plan = this.proposalPlans.get(key);
|
|
@@ -1450,7 +1571,7 @@ export class Room extends EventEmitter {
|
|
|
1450
1571
|
throw new Error("no such pending proposal");
|
|
1451
1572
|
if (proposal.status !== "pending")
|
|
1452
1573
|
return proposal;
|
|
1453
|
-
const what = [...proposal.settings.map((c) => c.key), ...proposal.vibemates.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1574
|
+
const what = [...proposal.settings.map((c) => c.key), ...proposal.vibemates.map((o) => `${o.op} ${o.name}`), ...(proposal.appearance ?? []).map((c) => c.key)].join(", ");
|
|
1454
1575
|
if (!accept) {
|
|
1455
1576
|
proposal.status = "rejected";
|
|
1456
1577
|
this.proposalPlans.delete(key);
|
|
@@ -1505,6 +1626,16 @@ export class Room extends EventEmitter {
|
|
|
1505
1626
|
patch[c.key] = c.key === "language" ? c.to : c.to;
|
|
1506
1627
|
this.updateSettings(patch);
|
|
1507
1628
|
}
|
|
1629
|
+
if (plan.appearance) {
|
|
1630
|
+
try {
|
|
1631
|
+
if (!this.skills?.appearance)
|
|
1632
|
+
throw new Error("looks are not available in this hub");
|
|
1633
|
+
this.skills.appearance.apply(plan.appearance);
|
|
1634
|
+
}
|
|
1635
|
+
catch (error) {
|
|
1636
|
+
skipped.push(`appearance (${error instanceof Error ? error.message : String(error)})`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1508
1639
|
proposal.status = "applied";
|
|
1509
1640
|
proposal.skipped = skipped;
|
|
1510
1641
|
this.proposalPlans.delete(key);
|
|
@@ -1697,10 +1828,9 @@ export class Room extends EventEmitter {
|
|
|
1697
1828
|
if (!this.skills)
|
|
1698
1829
|
return { ok: false, reason: "skills are not available in this hub" };
|
|
1699
1830
|
const skill = this.skills.library.get(name);
|
|
1700
|
-
const builtin = !!skill && skill.author === BUILTIN_AUTHOR && !skill.draft;
|
|
1701
1831
|
const mine = this.attachedSkills(participant).filter((s) => s.agentInvocable).map((s) => s.name);
|
|
1702
1832
|
const list = mine.length ? `your skills: ${mine.join(", ")}` : "you have no skills";
|
|
1703
|
-
if (!
|
|
1833
|
+
if (!this.hasSkill(participant, name) || !mine.some((s) => s.toLowerCase() === name.toLowerCase())) {
|
|
1704
1834
|
return { ok: false, reason: `"${name}" is not one of your skills (${list})` };
|
|
1705
1835
|
}
|
|
1706
1836
|
if (!skill || skill.problems.length)
|
package/dist/server.js
CHANGED
|
@@ -137,6 +137,12 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
137
137
|
}
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
+
if (req.method === "GET" && path === "/looks-custom.css") {
|
|
141
|
+
const css = await hub.looks.css();
|
|
142
|
+
res.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
|
|
143
|
+
res.end(css);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
140
146
|
if (req.method === "GET" && STATIC_FILES[path]) {
|
|
141
147
|
const entry = STATIC_FILES[path];
|
|
142
148
|
const body = await readFile(staticPath(entry));
|
|
@@ -360,6 +366,15 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
360
366
|
sendJson(res, 200, target.room.describeRoomForAgent(target.participantId));
|
|
361
367
|
return;
|
|
362
368
|
}
|
|
369
|
+
if (req.method === "GET" && path === "/api/mcp/looks") {
|
|
370
|
+
const target = hub.resolveMcpToken(url.searchParams.get("token") ?? "");
|
|
371
|
+
if (!target) {
|
|
372
|
+
sendJson(res, 403, { error: "unknown skills token (the session it belonged to is gone)" });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
sendJson(res, 200, await target.room.describeLooksForAgent(target.participantId));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
363
378
|
if (req.method === "GET" && path === "/api/mcp/message") {
|
|
364
379
|
const target = hub.resolveMcpToken(url.searchParams.get("token") ?? "");
|
|
365
380
|
if (!target) {
|
|
@@ -410,6 +425,14 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
410
425
|
sendJson(res, 200, { templates: hub.templates.list() });
|
|
411
426
|
return;
|
|
412
427
|
}
|
|
428
|
+
if (req.method === "GET" && path === "/api/looks") {
|
|
429
|
+
sendJson(res, 200, { looks: hub.looks.list() });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (req.method === "GET" && path === "/api/looks/describe") {
|
|
433
|
+
sendJson(res, 200, await hub.looks.describe());
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
413
436
|
if (req.method === "GET" && path === "/api/rooms") {
|
|
414
437
|
sendJson(res, 200, [...hub.rooms.values()].map((r) => r.snapshot()));
|
|
415
438
|
return;
|
|
@@ -423,6 +446,29 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
423
446
|
sendJson(res, 200, { ok: true, settings: hub.updateSettings(body) });
|
|
424
447
|
return;
|
|
425
448
|
}
|
|
449
|
+
if (path === "/api/looks/check") {
|
|
450
|
+
const { checkLookSpec } = await import("./looks.js");
|
|
451
|
+
try {
|
|
452
|
+
const checked = await checkLookSpec(body.spec ?? body);
|
|
453
|
+
sendJson(res, 200, { ok: checked.lint.ok, id: checked.spec.id, errors: checked.lint.errors, warnings: checked.lint.warnings, report: checked.lint.report });
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
sendJson(res, 200, { ok: false, errors: [{ level: "error", key: "spec", message: error instanceof Error ? error.message : String(error) }], warnings: [], report: [] });
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (path === "/api/looks") {
|
|
461
|
+
const saved = await hub.saveLook(body.spec ?? body, { author: "human", replace: body.replace === true || body.replace === "true" });
|
|
462
|
+
sendJson(res, 200, { ok: true, look: saved.spec, warnings: saved.lint.warnings, report: saved.lint.report });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (path === "/api/looks/remove") {
|
|
466
|
+
const id = String(body.id ?? "");
|
|
467
|
+
if (!hub.removeLook(id))
|
|
468
|
+
throw new Error(`no look "${id}" of your own to remove`);
|
|
469
|
+
sendJson(res, 200, { ok: true });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
426
472
|
if (path === "/api/fs/mkdir") {
|
|
427
473
|
sendJson(res, 200, { ok: true, path: await createFolder(String(body.parent ?? ""), String(body.name ?? "")) });
|
|
428
474
|
return;
|
|
@@ -498,6 +544,30 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
498
544
|
sendJson(res, 200, target.room.createTemplateForAgent(target.participantId, design, body.replace === true || body.replace === "true"));
|
|
499
545
|
return;
|
|
500
546
|
}
|
|
547
|
+
if (path === "/api/mcp/looks/lint" || path === "/api/mcp/looks/create" || path === "/api/mcp/looks/propose") {
|
|
548
|
+
const target = hub.resolveMcpToken(String(body.token ?? ""));
|
|
549
|
+
if (!target) {
|
|
550
|
+
sendJson(res, 403, { error: "unknown skills token (the session it belonged to is gone)" });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (path === "/api/mcp/looks/lint") {
|
|
554
|
+
sendJson(res, 200, await target.room.lintLookForAgent(target.participantId, body.spec ?? body));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (path === "/api/mcp/looks/create") {
|
|
558
|
+
sendJson(res, 200, await target.room.createLookForAgent(target.participantId, body.spec ?? body, body.replace === true || body.replace === "true"));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const num = (v) => (v === undefined || v === null || v === "" ? undefined : Number(v));
|
|
562
|
+
sendJson(res, 200, await target.room.proposeLookChanges(target.participantId, String(body.why ?? ""), {
|
|
563
|
+
look: typeof body.look === "string" ? body.look : undefined,
|
|
564
|
+
adjust: body.adjust && typeof body.adjust === "object" && !Array.isArray(body.adjust) ? body.adjust : undefined,
|
|
565
|
+
chatFontSize: num(body.chatFontSize),
|
|
566
|
+
font: typeof body.font === "string" ? body.font : undefined,
|
|
567
|
+
mono: typeof body.mono === "string" ? body.mono : undefined,
|
|
568
|
+
}));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
501
571
|
if (path === "/api/mcp/propose") {
|
|
502
572
|
const target = hub.resolveMcpToken(String(body.token ?? ""));
|
|
503
573
|
if (!target) {
|
package/dist/skills.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { isReservedSkillName, RESERVED_SKILL_NAMES } from "./commands.js";
|
|
5
5
|
export const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
6
6
|
export const SKILL_FILE = "SKILL.md";
|
|
7
|
-
import { ROOM_DESIGNER_NAME } from "./persona.js";
|
|
7
|
+
import { LOOK_DESIGNER_NAME, ROOM_DESIGNER_NAME } from "./persona.js";
|
|
8
8
|
export const BUILTIN_AUTHOR = "viberoom";
|
|
9
9
|
export const HUMAN_AUTHOR = "human";
|
|
10
10
|
const DESCRIPTION_MAX = 300;
|
|
@@ -175,7 +175,28 @@ export function renderSkillBody(body, args) {
|
|
|
175
175
|
.replace(/\r\n/g, "\n")
|
|
176
176
|
.trim();
|
|
177
177
|
}
|
|
178
|
-
export const
|
|
178
|
+
export const LOOK_DESIGNER = {
|
|
179
|
+
name: LOOK_DESIGNER_NAME,
|
|
180
|
+
description: "How to design a good viberoom look (how the window is drawn: colours, light, corners, fonts) as data that extends a shipped look. Load it before lint_look, create_look or propose_look_changes.",
|
|
181
|
+
argumentHint: "",
|
|
182
|
+
body: [
|
|
183
|
+
"A look is data, not code: which shipped look it extends, a few hues laid over that look's palette, and what it wants otherwise (corners, fonts, shadows, the chat's paper, a part of an element). Everything the window draws is derived from the palette, so a hue changed there reaches every place that wears it; start from the shipped look closest to what is asked and change as little as says it. The facts (the looks, every hue and element with what it means, the value syntax, the fonts, how the window is set now) come from describe_looks; never guess a name, a key the look does not have is refused.",
|
|
184
|
+
"",
|
|
185
|
+
"Name what a colour looks like, not what it is for: the palette says primary, ink, bg, white, lav; the elements say which part wears which hue (bubble.bg, btn.hoverInk). Prefer changing hues over changing elements: a new accent is palette.primary plus its primaryLight, primaryDeep and primaryDark steps and its tints lav and lav2; a new paper is bg with white (the panels) a shade apart from it, and soft / softer a step off white; a new ink is ink with ink2 for the words in a bubble and ink3, muted and faint growing quieter. Change an element only when a part must differ from what the palette gives it.",
|
|
186
|
+
"",
|
|
187
|
+
"You cannot see the colours; the lint can. Every look must read: the words on a bubble and on the paper at 7:1, the quiet words at 3:1 (4.5:1 on dark paper), every ink on its own paper at 3:1. Run lint_look before you save and read the report: it names every pair with its ratio. Fix the ink before the paper (a darker muted, an ink2 nearer black), keep the accent readable where it is written on (btn.onPrimary on primary at 3:1), and give a bubble that sits on paper of nearly its own shade a hairline (bubble.border) or another shade.",
|
|
188
|
+
"",
|
|
189
|
+
"Light comes from one place, top-left, and the shadows fall from it in the palette's shadowInk; a flat look sets the shadows to none, a look with volume raises a thing with a soft shadow below it and a bevel (a light gradient over its face) and sinks a pressed one with an inset shade. Say scheme dark when the paper is dark: diagrams, marks and the quiet-word floors follow it. One accent; the state colours (green ready, yellow thinking, red error, blue waiting, violet writing) keep their families, only their shades follow the paper. Fonts by id from describe_looks (they ship with viberoom and look the same on every machine); running text at lineHeight 1.45 or more; a terminal look wants the mono font everywhere and no curves (rScale 0), a soft look rounder corners (rScale 1.2-1.6) and pills for every control (rCtlMin 99px).",
|
|
190
|
+
"",
|
|
191
|
+
"When it reads: create_look saves it among the human's own looks (Settings → Appearance, after the shipped ones, with the human's name on it); one of the human's own may be replaced with replace: true, a shipped look never. Then propose_look_changes offers it as a card (look: its id), or fine-tunes any look with the adjustables describe_looks lists (a colour as #rrggbb, a scale 0-2) — the whole window changes, not this room, and only on the human's click. What a look cannot do: no CSS of its own, no layout, no icons, no fonts beyond the shipped ids; when the human asks for such a thing, say so instead of forcing a token to carry it.",
|
|
192
|
+
].join("\n"),
|
|
193
|
+
userInvocable: true,
|
|
194
|
+
agentInvocable: true,
|
|
195
|
+
author: BUILTIN_AUTHOR,
|
|
196
|
+
reviewed: true,
|
|
197
|
+
draft: false,
|
|
198
|
+
};
|
|
199
|
+
export const BUILTIN_SKILLS = [SKILL_WRITER, ROOM_DESIGNER, LOOK_DESIGNER];
|
|
179
200
|
export function isBuiltinSkill(name) {
|
|
180
201
|
const lower = name.trim().toLowerCase();
|
|
181
202
|
return BUILTIN_SKILLS.some((b) => b.name === lower);
|
package/package.json
CHANGED
package/ui/app.css
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
@supports not selector(::-webkit-scrollbar) { .rail-rooms { scrollbar-width: thin; } }
|
|
31
31
|
.shell.rail-open .rail-rooms { align-items: stretch; }
|
|
32
32
|
.rail-room { flex: none; }
|
|
33
|
-
.rail-room [data-ui="room-mark"] { width: 40px; height: 40px; border-radius:
|
|
33
|
+
.rail-room [data-ui="room-mark"] { width: 40px; height: 40px; border-radius: var(--room-mark-radius-rail); font-size: 16px; opacity: 0.72; transition: opacity var(--t-fast), transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast); }
|
|
34
34
|
.rail-room [data-ui="room-mark"][data-kind="emoji"] { font-size: 21px; }
|
|
35
35
|
.rail-room:hover [data-ui="room-mark"] { opacity: 1; transform: translateY(-1px); }
|
|
36
36
|
.rail-room.active, .rail-room.active:hover { background: transparent; box-shadow: none; }
|
|
@@ -231,18 +231,18 @@
|
|
|
231
231
|
.home-room .hr-time { color: var(--faint); font-size: calc(11px * var(--fs-scale)); font-weight: 700; flex: none; }
|
|
232
232
|
.home-room.empty-room { cursor: default; }
|
|
233
233
|
.feature-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
|
234
|
-
.feature { border-radius: calc(18px * var(--r-scale)); padding: 18px 18px 16px; display: flex; flex-direction: column; gap: 8px; box-shadow: var(--
|
|
234
|
+
.feature { border-radius: calc(18px * var(--r-scale)); padding: 18px 18px 16px; display: flex; flex-direction: column; gap: 8px; box-shadow: var(--feature-shadow); background-image: var(--bevel); transition: transform var(--t-fast) var(--ease-out); }
|
|
235
235
|
.feature:hover { transform: translateY(-2px); }
|
|
236
236
|
.feature .f-emoji { width: 46px; height: 46px; border-radius: calc(14px * var(--r-scale)); background: var(--feature-emoji-bg); display: grid; place-content: center; font-size: 24px; box-shadow: var(--shadow-tile); }
|
|
237
237
|
.feature h3 { font-size: calc(15px * var(--fs-scale)); margin-top: 4px; }
|
|
238
238
|
.feature p { margin: 0; font-size: calc(13px * var(--fs-scale)); font-weight: 600; line-height: 1.5; color: var(--ink-2); }
|
|
239
|
-
.feature.lav { background: var(--lav); }
|
|
240
|
-
.feature.mint { background: var(--mint); }
|
|
241
|
-
.feature.warm { background: var(--warm); }
|
|
242
|
-
.feature.peach { background: var(--feature-peach-bg); }
|
|
243
|
-
.feature.rose { background: var(--rose); }
|
|
244
|
-
.feature.sky { background: var(--sky); }
|
|
245
|
-
.feature.orchid { background: var(--orchid); }
|
|
239
|
+
.feature.lav { background-color: var(--lav); }
|
|
240
|
+
.feature.mint { background-color: var(--mint); }
|
|
241
|
+
.feature.warm { background-color: var(--warm); }
|
|
242
|
+
.feature.peach { background-color: var(--feature-peach-bg); }
|
|
243
|
+
.feature.rose { background-color: var(--rose); }
|
|
244
|
+
.feature.sky { background-color: var(--sky); }
|
|
245
|
+
.feature.orchid { background-color: var(--orchid); }
|
|
246
246
|
.home-vendors { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
|
|
247
247
|
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
|
248
248
|
.step { display: flex; gap: 14px; align-items: flex-start; background: var(--step-bg); border-radius: calc(18px * var(--r-scale)); padding: 16px 18px; box-shadow: var(--edge); }
|
|
@@ -362,6 +362,13 @@
|
|
|
362
362
|
.look-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
363
363
|
.look-cards [data-ui="look-card"] { width: auto; }
|
|
364
364
|
#look-dialog .look-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 6px 0 14px; }
|
|
365
|
+
.look-own { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 8px 0 2px; }
|
|
366
|
+
.look-own .own-only { display: inline-flex; gap: 6px; }
|
|
367
|
+
.prop-window { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; font-size: calc(12px * var(--fs-scale)); font-weight: 600; }
|
|
368
|
+
.prop-window .note { display: flex; align-items: center; gap: 6px; }
|
|
369
|
+
.prop-window .note > .i { width: 14px; height: 14px; flex: none; }
|
|
370
|
+
.prop-look { display: flex; align-items: center; gap: 10px; }
|
|
371
|
+
.prop-look [data-ui="look-card"] { width: 150px; cursor: default; }
|
|
365
372
|
.look-adjust { margin-top: 4px; }
|
|
366
373
|
.look-adjust .adjust-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
367
374
|
.look-adjust .adjust-head .label { font-size: calc(12px * var(--fs-scale)); font-weight: 800; color: var(--ink-3); }
|
|
@@ -373,14 +380,10 @@
|
|
|
373
380
|
.adjust-group.text .field.row [data-ui="number-field"] { width: 104px; flex: none; }
|
|
374
381
|
.adjust-group.text .field.row select { flex: 0 1 58%; }
|
|
375
382
|
.adjust-group.text #sp-chat-sample { margin-top: 4px; }
|
|
376
|
-
.bubble { max-width: 100%; background: var(--bubble-bg); border-radius: calc(6px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)); padding: 12px 16px; min-width: 0; font-size: calc(14.5px * var(--fs-scale)); font-weight: 600; line-height: 1.55; color: var(--bubble-ink); box-shadow: inset 0 0 0 1px var(--bubble-border), var(--bubble-shadow); }
|
|
377
|
-
.msg.mine .bubble { background: var(--bubble-mine-bg); color: var(--bubble-mine-ink); border-radius: calc(18px * var(--r-scale)) calc(6px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)); box-shadow: inset 0 0 0 1px var(--bubble-border), var(--bubble-mine-shadow); padding: 11px 16px; }
|
|
383
|
+
.bubble { max-width: 100%; background-color: var(--bubble-bg); background-image: var(--bevel); border-radius: calc(6px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)); padding: 12px 16px; min-width: 0; font-size: calc(14.5px * var(--fs-scale)); font-weight: 600; line-height: 1.55; color: var(--bubble-ink); box-shadow: inset 0 0 0 1px var(--bubble-border), var(--bubble-shadow); }
|
|
384
|
+
.msg.mine .bubble { background-color: transparent; background-image: var(--bevel), var(--bubble-mine-bg); color: var(--bubble-mine-ink); border-radius: calc(18px * var(--r-scale)) calc(6px * var(--r-scale)) calc(18px * var(--r-scale)) calc(18px * var(--r-scale)); box-shadow: inset 0 0 0 1px var(--bubble-border), var(--bubble-mine-shadow); padding: 11px 16px; }
|
|
378
385
|
.msg.mine .bubble .text { color: var(--bubble-mine-ink); }
|
|
379
386
|
.msg.mine.waiting .bubble { background: var(--attention-grad); box-shadow: var(--attention-shadow); }
|
|
380
|
-
html[data-look="plush"] .msg .bubble { background-image: var(--bevel); }
|
|
381
|
-
html[data-look="plush"] .msg.mine .bubble { background-image: var(--bevel), var(--bubble-mine-bg); }
|
|
382
|
-
html[data-look="plush"] .composer-box { box-shadow: var(--input-shadow); background: var(--softer); border-radius: calc(14px * var(--r-scale)); padding: 0 12px; }
|
|
383
|
-
html[data-look="plush"] .feature { box-shadow: var(--shadow-card); background-image: var(--bevel); }
|
|
384
387
|
.bubble .waiting { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--bubble-mine-rule); font-size: calc(13px * var(--fs-scale)); font-weight: 600; color: var(--bubble-mine-waiting-ink); }
|
|
385
388
|
.bubble .waiting .i { width: 14px; height: 14px; vertical-align: -2px; }
|
|
386
389
|
.bubble .waiting-text { flex: 1; min-width: 160px; }
|
|
@@ -543,7 +546,7 @@ table.csv tbody tr:nth-child(even) td { background: var(--table-stripe); }
|
|
|
543
546
|
.composer .smile-btn { border: 0; background: transparent; color: var(--placeholder); padding: 0; display: grid; place-content: center; width: 28px; height: 28px; border-radius: calc(8px * var(--r-scale)); flex: none; }
|
|
544
547
|
.composer .smile-btn .i { width: 20px; height: 20px; }
|
|
545
548
|
.composer .smile-btn:hover { color: var(--primary); }
|
|
546
|
-
.composer-box { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
549
|
+
.composer-box { flex: 1; min-width: 0; display: flex; align-items: center; box-shadow: var(--composer-shadow); background: var(--composer-bg); border-radius: var(--composer-radius); padding: 0 var(--composer-pad-x); }
|
|
547
550
|
.composer-box:has(.shots-tray:not([hidden])) { flex-direction: column; align-items: stretch; }
|
|
548
551
|
.composer-box:has(.shots-tray:not([hidden])) textarea { flex: none; }
|
|
549
552
|
.composer.drop-target { box-shadow: 0 0 0 2px var(--primary); }
|
|
@@ -636,6 +639,7 @@ table.csv tbody tr:nth-child(even) td { background: var(--table-stripe); }
|
|
|
636
639
|
#fv-tools label.search > input:not([type="checkbox"]) { flex: 1 1 auto; margin-top: 0; width: auto; height: 100%; padding: 0; border: 0; background: transparent; }
|
|
637
640
|
.fv-tools .fv-hits, .fv-tools .fv-count { color: var(--faint); font-size: var(--fs-xs); font-weight: 700; }
|
|
638
641
|
.fv-tools .fv-goto, .fv-tools .fv-wrap { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: var(--fs-xs); font-weight: 700; }
|
|
642
|
+
#fv-tools .fv-goto, #fv-tools .fv-wrap { margin: 0; }
|
|
639
643
|
#fv-tools .fv-goto > input[type="number"] { flex: 0 0 76px; margin-top: 0; width: 76px; height: 30px; padding: 0 8px; }
|
|
640
644
|
.fv-tools .fv-wrap input { accent-color: var(--primary); }
|
|
641
645
|
.text [data-ui="file-card"] { margin: 8px 0 4px; }
|