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.
- package/README.md +1 -1
- package/dist/acp-client.js +3 -0
- package/dist/commands.js +31 -0
- package/dist/edit.js +10 -1
- package/dist/files.js +60 -0
- package/dist/hub.js +73 -2
- package/dist/persona.js +82 -12
- package/dist/room.js +213 -25
- package/dist/server.js +102 -5
- package/dist/skills.js +4 -0
- package/dist/templates.js +76 -0
- package/package.json +2 -1
- package/templates/design-critique/template.json +24 -0
- package/templates/explainer/template.json +17 -0
- package/templates/forge-and-lumen/template.json +28 -0
- package/templates/pair-programmer/template.json +17 -0
- package/ui/app.css +175 -20
- package/ui/app.js +868 -116
- package/ui/icons.js +13 -10
- package/ui/index.html +31 -3
- package/ui/theme.css +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Open a room, summon the agents you already have, give each one a role, and let t
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="
|
|
17
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A Forge & Lumen room: Forge explains a git command from a screenshot, Lumen adds the part that changes whose problem it is">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
package/dist/acp-client.js
CHANGED
|
@@ -85,6 +85,9 @@ export class AcpAgent {
|
|
|
85
85
|
get supportsLoadSession() {
|
|
86
86
|
return this.initResult?.agentCapabilities?.loadSession === true;
|
|
87
87
|
}
|
|
88
|
+
get acceptsImages() {
|
|
89
|
+
return this.initResult?.agentCapabilities?.promptCapabilities?.image === true;
|
|
90
|
+
}
|
|
88
91
|
loadSession(sessionId, cwd, mcpServers = []) {
|
|
89
92
|
return this.peer.request("session/load", { sessionId, cwd, mcpServers }).then((result) => ({
|
|
90
93
|
...(result ?? {}),
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
|
+
export const ROOM_COMMANDS = ["respawn"];
|
|
3
|
+
export const RESERVED_SKILL_NAMES = [
|
|
4
|
+
"respawn",
|
|
5
|
+
"clear",
|
|
6
|
+
"help",
|
|
7
|
+
"brief",
|
|
8
|
+
"mute",
|
|
9
|
+
"unmute",
|
|
10
|
+
"invite",
|
|
11
|
+
"kick",
|
|
12
|
+
"reset",
|
|
13
|
+
"stop",
|
|
14
|
+
"silent",
|
|
15
|
+
"request-brief",
|
|
16
|
+
];
|
|
17
|
+
export function isReservedSkillName(name) {
|
|
18
|
+
return RESERVED_SKILL_NAMES.includes(name.trim().toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
export function parseRoomCommand(text) {
|
|
21
|
+
const match = text.trim().match(/^\/([A-Za-z][A-Za-z0-9_-]{0,31})(?:\s+([\s\S]*))?$/);
|
|
22
|
+
if (!match)
|
|
23
|
+
return null;
|
|
24
|
+
const name = match[1].toLowerCase();
|
|
25
|
+
if (!ROOM_COMMANDS.includes(name))
|
|
26
|
+
return null;
|
|
27
|
+
return { name, args: (match[2] ?? "").trim() };
|
|
28
|
+
}
|
|
29
|
+
export function commandTarget(args) {
|
|
30
|
+
return args.trim().replace(/^@/, "").trim();
|
|
31
|
+
}
|
package/dist/edit.js
CHANGED
|
@@ -20,12 +20,21 @@ export function affectedByEdit(agents, editedSeq) {
|
|
|
20
20
|
return { restart, untouched, offline };
|
|
21
21
|
}
|
|
22
22
|
const QUOTE_MAX = 240;
|
|
23
|
+
const FULL_MAX = 4000;
|
|
23
24
|
function quote(text) {
|
|
24
25
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
25
26
|
return flat.length > QUOTE_MAX ? `${flat.slice(0, QUOTE_MAX)}…` : flat;
|
|
26
27
|
}
|
|
28
|
+
function full(text) {
|
|
29
|
+
const trimmed = text.trim();
|
|
30
|
+
return trimmed.length > FULL_MAX ? `${trimmed.slice(0, FULL_MAX)}…` : trimmed;
|
|
31
|
+
}
|
|
27
32
|
export function editNotice(humanName, previous, next) {
|
|
28
|
-
|
|
33
|
+
const added = next.startsWith(previous) ? next.slice(previous.length).trim() : "";
|
|
34
|
+
const body = added
|
|
35
|
+
? `They added to the end of it:\n\n"${full(added)}"`
|
|
36
|
+
: `It now reads:\n\n"${full(next)}"\n\nBefore: "${quote(previous)}"`;
|
|
37
|
+
return `${humanName} edited an earlier message. ${body}\n\nReply only if the change matters to you; otherwise [silent].`;
|
|
29
38
|
}
|
|
30
39
|
export function rewriteNotice(humanName, removedCount, restarted) {
|
|
31
40
|
const removed = removedCount === 1 ? "1 later message" : `${removedCount} later messages`;
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export const IMAGE_TYPES = {
|
|
6
|
+
"image/png": "png",
|
|
7
|
+
"image/jpeg": "jpg",
|
|
8
|
+
"image/webp": "webp",
|
|
9
|
+
"image/gif": "gif",
|
|
10
|
+
};
|
|
11
|
+
export const IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
export const IMAGES_PER_MESSAGE = 6;
|
|
13
|
+
const STORED_NAME = /^[0-9a-f]{32}\.(png|jpg|webp|gif)$/;
|
|
14
|
+
export function isStoredFileName(name) {
|
|
15
|
+
return STORED_NAME.test(name);
|
|
16
|
+
}
|
|
17
|
+
export function contentTypeOf(file) {
|
|
18
|
+
const ext = file.slice(file.lastIndexOf(".") + 1);
|
|
19
|
+
for (const [type, e] of Object.entries(IMAGE_TYPES))
|
|
20
|
+
if (e === ext)
|
|
21
|
+
return type;
|
|
22
|
+
return "application/octet-stream";
|
|
23
|
+
}
|
|
24
|
+
function decode(data) {
|
|
25
|
+
const comma = data.startsWith("data:") ? data.indexOf(",") : -1;
|
|
26
|
+
const base64 = comma >= 0 ? data.slice(comma + 1) : data;
|
|
27
|
+
const buffer = Buffer.from(base64, "base64");
|
|
28
|
+
if (!buffer.length)
|
|
29
|
+
throw new Error("the image is empty");
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
function labelFor(name, ext, hash) {
|
|
33
|
+
const trimmed = (name ?? "").trim().replace(/[\r\n\t]/g, " ");
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return `pasted-${hash.slice(0, 6)}.${ext}`;
|
|
36
|
+
return trimmed.length > 80 ? `${trimmed.slice(0, 77)}…` : trimmed;
|
|
37
|
+
}
|
|
38
|
+
export function saveImage(dir, input) {
|
|
39
|
+
const ext = IMAGE_TYPES[input.mimeType];
|
|
40
|
+
if (!ext)
|
|
41
|
+
throw new Error(`unsupported image type: ${input.mimeType || "unknown"} (png, jpeg, webp and gif only)`);
|
|
42
|
+
const buffer = decode(input.data);
|
|
43
|
+
if (buffer.length > IMAGE_MAX_BYTES) {
|
|
44
|
+
throw new Error(`the image is too large (${Math.round(buffer.length / 1024)} kB; the limit is ${IMAGE_MAX_BYTES / 1024 / 1024} MB)`);
|
|
45
|
+
}
|
|
46
|
+
const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 32);
|
|
47
|
+
const file = `${hash}.${ext}`;
|
|
48
|
+
const target = join(dir, file);
|
|
49
|
+
if (!existsSync(target))
|
|
50
|
+
writeFileSync(target, buffer);
|
|
51
|
+
const attachment = { file, name: labelFor(input.name, ext, hash), mimeType: input.mimeType, bytes: buffer.length };
|
|
52
|
+
if (Number.isInteger(input.n) && input.n > 0)
|
|
53
|
+
attachment.n = input.n;
|
|
54
|
+
return attachment;
|
|
55
|
+
}
|
|
56
|
+
export function saveImages(dir, inputs) {
|
|
57
|
+
if (inputs.length > IMAGES_PER_MESSAGE)
|
|
58
|
+
throw new Error(`up to ${IMAGES_PER_MESSAGE} images per message`);
|
|
59
|
+
return inputs.map((input) => saveImage(dir, input));
|
|
60
|
+
}
|
package/dist/hub.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.js";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,7 @@ import { listRecipes } from "./recipes.js";
|
|
|
10
10
|
import { DEFAULT_ROOM_SETTINGS } from "./persona.js";
|
|
11
11
|
import { Room } from "./room.js";
|
|
12
12
|
import { SkillLibrary } from "./skills.js";
|
|
13
|
+
import { TemplateLibrary } from "./templates.js";
|
|
13
14
|
export const DIAGRAM_PRESETS = ["pop", "lavender", "mint", "sunset", "slate"];
|
|
14
15
|
const ROOM_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
15
16
|
const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules"];
|
|
@@ -17,6 +18,7 @@ export class Hub extends EventEmitter {
|
|
|
17
18
|
dataDir;
|
|
18
19
|
rooms = new Map();
|
|
19
20
|
skills;
|
|
21
|
+
templates;
|
|
20
22
|
settings;
|
|
21
23
|
log;
|
|
22
24
|
optionCache = new Map();
|
|
@@ -29,6 +31,7 @@ export class Hub extends EventEmitter {
|
|
|
29
31
|
this.log = log;
|
|
30
32
|
mkdirSync(join(this.dataDir, "rooms"), { recursive: true });
|
|
31
33
|
this.skills = new SkillLibrary(join(this.dataDir, "skills"), log.child("skills"));
|
|
34
|
+
this.templates = new TemplateLibrary(join(this.dataDir, "templates"), log.child("templates"));
|
|
32
35
|
try {
|
|
33
36
|
this.skills.seedBuiltins();
|
|
34
37
|
}
|
|
@@ -304,19 +307,86 @@ export class Hub extends EventEmitter {
|
|
|
304
307
|
this.log.info(`created room "${name}" (${id}) with workspace ${dir}`);
|
|
305
308
|
return { room, notices };
|
|
306
309
|
}
|
|
310
|
+
async createRoomFromTemplate(input) {
|
|
311
|
+
const template = this.templates.get(input.templateId);
|
|
312
|
+
if (!template)
|
|
313
|
+
throw new Error(`no such template: ${input.templateId}`);
|
|
314
|
+
const { room, notices } = this.createRoom({ name: input.name, dir: input.dir, settings: template.settings });
|
|
315
|
+
for (const [i, tv] of template.vibemates.entries()) {
|
|
316
|
+
const choice = input.vibemates[i] ?? { name: tv.name, agentType: "" };
|
|
317
|
+
const skills = (tv.skills ?? []).filter((name) => {
|
|
318
|
+
const ok = !!this.skills.get(name);
|
|
319
|
+
if (!ok)
|
|
320
|
+
notices.push(`${choice.name || tv.name}: skill "${name}" is not in the library; not attached.`);
|
|
321
|
+
return ok;
|
|
322
|
+
});
|
|
323
|
+
if (!choice.agentType) {
|
|
324
|
+
try {
|
|
325
|
+
room.addUnstaffed({ name: choice.name || tv.name, tagline: tv.tagline, role: tv.role, avatar: tv.avatar, skills });
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
notices.push(`${choice.name || tv.name} could not be added: ${error instanceof Error ? error.message : String(error)}`);
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
await room.inviteAgent({
|
|
334
|
+
agentType: choice.agentType,
|
|
335
|
+
name: choice.name || tv.name,
|
|
336
|
+
tagline: tv.tagline,
|
|
337
|
+
role: tv.role,
|
|
338
|
+
avatar: tv.avatar,
|
|
339
|
+
skills,
|
|
340
|
+
model: choice.model ?? tv.model,
|
|
341
|
+
effort: choice.effort ?? tv.effort,
|
|
342
|
+
mode: choice.mode ?? tv.mode,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
notices.push(`${choice.name || tv.name} could not be summoned: ${error instanceof Error ? error.message : String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
this.saveRooms();
|
|
350
|
+
return { room, notices };
|
|
351
|
+
}
|
|
307
352
|
getRoom(id) {
|
|
308
353
|
const room = this.rooms.get(id);
|
|
309
354
|
if (!room)
|
|
310
355
|
throw new Error(`no such room: ${id}`);
|
|
311
356
|
return room;
|
|
312
357
|
}
|
|
358
|
+
openRooms = [];
|
|
359
|
+
markOpened(id) {
|
|
360
|
+
this.getRoom(id);
|
|
361
|
+
if (this.openRooms.includes(id))
|
|
362
|
+
return;
|
|
363
|
+
this.openRooms.push(id);
|
|
364
|
+
this.emit("event", { type: "rooms.opened", roomIds: [...this.openRooms] });
|
|
365
|
+
}
|
|
313
366
|
async removeRoom(id) {
|
|
314
367
|
const room = this.getRoom(id);
|
|
315
368
|
await room.shutdown();
|
|
316
369
|
this.rooms.delete(id);
|
|
370
|
+
const at = this.openRooms.indexOf(id);
|
|
371
|
+
if (at >= 0)
|
|
372
|
+
this.openRooms.splice(at, 1);
|
|
317
373
|
this.saveRooms();
|
|
318
374
|
this.emit("event", { type: "room.removed", roomId: id });
|
|
319
|
-
|
|
375
|
+
const folder = join(this.dataDir, "rooms", id);
|
|
376
|
+
if (existsSync(folder)) {
|
|
377
|
+
const trash = join(this.dataDir, "trash");
|
|
378
|
+
mkdirSync(trash, { recursive: true });
|
|
379
|
+
const target = join(trash, `${id}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
380
|
+
try {
|
|
381
|
+
renameSync(folder, target);
|
|
382
|
+
this.log.info(`removed room ${id}; its folder is in ${target}`);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
this.log.warn(`removed room ${id}, but its folder could not be moved to trash: ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else
|
|
389
|
+
this.log.info(`removed room ${id}`);
|
|
320
390
|
}
|
|
321
391
|
snapshot() {
|
|
322
392
|
return {
|
|
@@ -325,6 +395,7 @@ export class Hub extends EventEmitter {
|
|
|
325
395
|
skills: this.skills.list(),
|
|
326
396
|
roomDefaults: { ...DEFAULT_ROOM_SETTINGS, ...this.settings.roomDefaults },
|
|
327
397
|
rooms: [...this.rooms.values()].map((room) => room.snapshot()),
|
|
398
|
+
openRooms: [...this.openRooms],
|
|
328
399
|
};
|
|
329
400
|
}
|
|
330
401
|
async shutdown() {
|
package/dist/persona.js
CHANGED
|
@@ -4,6 +4,10 @@ export const SILENT_MARKER = "[silent]";
|
|
|
4
4
|
export const REQUEST_BRIEF_MARKER = "[request-brief]";
|
|
5
5
|
export const SKILL_MARKER_PATTERN = /\[skill:\s*([A-Za-z0-9][A-Za-z0-9_-]{0,31})\s*\]/i;
|
|
6
6
|
export const SKILL_TOOL_NAME = "load_skill";
|
|
7
|
+
export function skillPull(reply) {
|
|
8
|
+
const match = reply.trim().match(SKILL_MARKER_PATTERN);
|
|
9
|
+
return match && match[0] === reply.trim() ? match[1] : null;
|
|
10
|
+
}
|
|
7
11
|
export const SKILL_WRITER_NAME = "skill-writer";
|
|
8
12
|
export const DEFAULT_ROOM_SETTINGS = {
|
|
9
13
|
topic: "",
|
|
@@ -38,6 +42,54 @@ export const BRIEF_AFFECTING_SETTINGS = [
|
|
|
38
42
|
"customRules",
|
|
39
43
|
"agentsWakeEachOther",
|
|
40
44
|
];
|
|
45
|
+
export const IMAGE_MARKER_PATTERN = /\[img\s+(\d+)\]/gi;
|
|
46
|
+
export function promptText(parts) {
|
|
47
|
+
return parts.map((p) => (p.type === "text" ? p.text : "")).join("");
|
|
48
|
+
}
|
|
49
|
+
function imageMarker(image) {
|
|
50
|
+
if (image.attached)
|
|
51
|
+
return `[img ${image.n}]`;
|
|
52
|
+
const who = image.forNames.length ? ` · for ${image.forNames.join(", ")}` : "";
|
|
53
|
+
return `[img ${image.n} · ${image.ref}${who} · ${image.path}]`;
|
|
54
|
+
}
|
|
55
|
+
function messageParts(line) {
|
|
56
|
+
const parts = [];
|
|
57
|
+
let text = "";
|
|
58
|
+
const flush = () => {
|
|
59
|
+
if (text)
|
|
60
|
+
parts.push({ type: "text", text });
|
|
61
|
+
text = "";
|
|
62
|
+
};
|
|
63
|
+
const place = (image) => {
|
|
64
|
+
text += imageMarker(image);
|
|
65
|
+
if (image.attached) {
|
|
66
|
+
flush();
|
|
67
|
+
parts.push({ type: "image", image });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const images = line.images ?? [];
|
|
71
|
+
const placed = new Set();
|
|
72
|
+
let last = 0;
|
|
73
|
+
for (const match of line.text.matchAll(IMAGE_MARKER_PATTERN)) {
|
|
74
|
+
const image = images.find((i) => i.n === Number(match[1]));
|
|
75
|
+
if (!image || placed.has(image.n))
|
|
76
|
+
continue;
|
|
77
|
+
placed.add(image.n);
|
|
78
|
+
text += line.text.slice(last, match.index);
|
|
79
|
+
place(image);
|
|
80
|
+
last = (match.index ?? 0) + match[0].length;
|
|
81
|
+
}
|
|
82
|
+
text += line.text.slice(last);
|
|
83
|
+
for (const image of images) {
|
|
84
|
+
if (placed.has(image.n))
|
|
85
|
+
continue;
|
|
86
|
+
if (!text.endsWith("\n") && (text || parts.length))
|
|
87
|
+
text += "\n";
|
|
88
|
+
place(image);
|
|
89
|
+
}
|
|
90
|
+
flush();
|
|
91
|
+
return parts;
|
|
92
|
+
}
|
|
41
93
|
export function ensureDir(dir) {
|
|
42
94
|
mkdirSync(dir, { recursive: true });
|
|
43
95
|
return dir;
|
|
@@ -51,6 +103,8 @@ function describeEntry(entry, settings) {
|
|
|
51
103
|
parts.push(entry.vendor);
|
|
52
104
|
if (entry.tagline)
|
|
53
105
|
parts.push(`"${entry.tagline}"`);
|
|
106
|
+
if (entry.muted)
|
|
107
|
+
parts.push("muted");
|
|
54
108
|
return `${entry.name} (${parts.join(" · ")})`;
|
|
55
109
|
}
|
|
56
110
|
function skillsSection(skills) {
|
|
@@ -139,7 +193,9 @@ export function buildHeader(settings, persona, roster, hops, notes, skills) {
|
|
|
139
193
|
.map((r) => {
|
|
140
194
|
if (r.name === persona.name)
|
|
141
195
|
return `${r.name} (you)`;
|
|
142
|
-
|
|
196
|
+
if (r.kind === "human")
|
|
197
|
+
return `${r.name} (human)`;
|
|
198
|
+
return r.muted ? `${r.name} (muted)` : r.name;
|
|
143
199
|
})
|
|
144
200
|
.join(", ");
|
|
145
201
|
const who = persona.tagline.trim() ? `${persona.name} (${persona.tagline.trim()})` : persona.name;
|
|
@@ -173,27 +229,41 @@ export function composeSkillBlock(parts) {
|
|
|
173
229
|
return lines.join("\n");
|
|
174
230
|
}
|
|
175
231
|
export function composePrompt(parts) {
|
|
176
|
-
const
|
|
232
|
+
const out = [];
|
|
233
|
+
const push = (text) => {
|
|
234
|
+
const lastPart = out[out.length - 1];
|
|
235
|
+
if (lastPart && lastPart.type === "text")
|
|
236
|
+
lastPart.text += text;
|
|
237
|
+
else
|
|
238
|
+
out.push({ type: "text", text });
|
|
239
|
+
};
|
|
177
240
|
if (parts.brief)
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
push(`${parts.brief}\n`);
|
|
242
|
+
push(`${parts.header}\n`);
|
|
180
243
|
for (const block of parts.skills ?? [])
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
push(`${block}\n`);
|
|
245
|
+
push("<messages>\n");
|
|
183
246
|
if (parts.omitted > 0)
|
|
184
|
-
|
|
247
|
+
push(`… ${parts.omitted} earlier messages omitted\n`);
|
|
185
248
|
for (const line of parts.backlog) {
|
|
186
249
|
if (line.kind === "event") {
|
|
187
|
-
|
|
250
|
+
push(`· ${line.text}\n`);
|
|
188
251
|
}
|
|
189
252
|
else {
|
|
190
253
|
const target = line.toNames && line.toNames.length ? ` -> ${line.toNames.map((t) => `@${t}`).join(" ")}` : "";
|
|
191
|
-
|
|
254
|
+
push(`${line.fromName}${target}: `);
|
|
255
|
+
for (const part of messageParts(line)) {
|
|
256
|
+
if (part.type === "text")
|
|
257
|
+
push(part.text);
|
|
258
|
+
else
|
|
259
|
+
out.push(part);
|
|
260
|
+
}
|
|
261
|
+
push("\n");
|
|
192
262
|
}
|
|
193
263
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return
|
|
264
|
+
push("</messages>\n");
|
|
265
|
+
push(`Reply as ${parts.personaName} (or ${SILENT_MARKER}).`);
|
|
266
|
+
return out;
|
|
197
267
|
}
|
|
198
268
|
export function composeCorrectionPrompt(parts) {
|
|
199
269
|
const lines = [];
|