Skip to content

Commit 88bd303

Browse files
committed
fix(personas): support persona names with spaces in :: prefix matching (#5349)
The previous regex [^\s:] excluded spaces, so 'Persona 1' would only capture 'Persona' and the lookup would fail. Replace the regex approach with a loop over the user's own personas (sorted longest-first), doing a case-insensitive startsWith check. Works for any name including spaces.
1 parent c547377 commit 88bd303

1 file changed

Lines changed: 30 additions & 24 deletions

File tree

src/socketHandlers/messages.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -603,34 +603,40 @@ module.exports = function register(socket, ctx) {
603603
}
604604

605605
// ── Persona detection (#86, #5349) ────────────────────
606-
// Pattern: "::PersonaName actual message body" (colon optional after the
607-
// name). The leading "::" is a deliberate, unambiguous trigger that does
608-
// not conflict with any markdown syntax (unlike ">>", which the markdown
609-
// renderer would turn into a nested blockquote). Persona names match
610-
// against the sender's own user_personas (case-insensitive). When a
611-
// match is found we strip the prefix from the stored content and stamp
612-
// persona_id / persona_username / persona_avatar so the outgoing message
613-
// displays as the persona while the real user_id (and therefore
614-
// moderation) remains intact.
606+
// Pattern: "::PersonaName actual message body"
607+
// The leading "::" is a deliberate, unambiguous trigger that does not
608+
// conflict with any markdown syntax. We iterate over the sender's own
609+
// personas (longest name first to handle prefix ambiguity) and do a
610+
// case-insensitive prefix match — this correctly handles persona names
611+
// that contain spaces, which a single regex with [^\s] cannot do.
615612
let personaId = null;
616613
let personaUsername = null;
617614
let personaAvatar = null;
618615
let finalContent = safeContent;
619-
{
620-
const m = safeContent.match(/^::\s*([^\s:][^\s:]{0,31}):?\s+([\s\S]+)$/);
621-
if (m) {
622-
const candidate = m[1].trim();
623-
const body = m[2];
624-
if (candidate && body && body.trim().length > 0) {
625-
const persona = db.prepare(
626-
'SELECT id, name, avatar FROM user_personas WHERE user_id = ? AND name = ? COLLATE NOCASE'
627-
).get(socket.user.id, candidate);
628-
if (persona) {
629-
personaId = persona.id;
630-
personaUsername = persona.name;
631-
personaAvatar = persona.avatar || null;
632-
finalContent = body.trim();
633-
}
616+
if (safeContent.startsWith('::')) {
617+
const userPersonas = db.prepare(
618+
'SELECT id, name, avatar FROM user_personas WHERE user_id = ?'
619+
).all(socket.user.id);
620+
// Sort longest name first to prefer the most specific match.
621+
userPersonas.sort((a, b) => b.name.length - a.name.length);
622+
const lower = safeContent.toLowerCase();
623+
for (const persona of userPersonas) {
624+
// Allow "::Name message" (space separator) or "::Name: message" (colon+space)
625+
const base = '::' + persona.name.toLowerCase();
626+
let body = null;
627+
if (lower.startsWith(base + ' ')) {
628+
body = safeContent.slice(base.length + 1).trim();
629+
} else if (lower.startsWith(base + ': ')) {
630+
body = safeContent.slice(base.length + 2).trim();
631+
} else if (lower.startsWith(base + ':') && safeContent.length > base.length + 1) {
632+
body = safeContent.slice(base.length + 1).trim();
633+
}
634+
if (body && body.length > 0) {
635+
personaId = persona.id;
636+
personaUsername = persona.name;
637+
personaAvatar = persona.avatar || null;
638+
finalContent = body;
639+
break;
634640
}
635641
}
636642
}

0 commit comments

Comments
 (0)