clay-server 2.35.1 → 2.36.0-beta.2
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 +136 -88
- package/lib/project-image.js +1 -1
- package/lib/project-mate-interaction.js +72 -19
- package/lib/project-notifications.js +28 -2
- package/lib/project-user-mention.js +193 -0
- package/lib/project.js +27 -0
- package/lib/public/app.js +8 -0
- package/lib/public/modules/app-messages.js +13 -1
- package/lib/public/modules/app-notifications.js +40 -4
- package/lib/public/modules/input.js +38 -3
- package/lib/public/modules/mention.js +290 -48
- package/lib/server.js +34 -0
- package/lib/yoke/adapters/claude-worker.js +69 -0
- package/lib/yoke/adapters/claude.js +30 -0
- package/package.json +21 -9
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// project-user-mention.js - User-to-user @mention handling within a session.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the @mate mention pattern (see project-mate-interaction.js) but for
|
|
4
|
+
// human users. Each user_mention is a one-shot side-channel message that:
|
|
5
|
+
// 1. Is broadcast to all session viewers as a `user_mention` event
|
|
6
|
+
// 2. Is recorded in session.history so it survives reload/replay
|
|
7
|
+
// 3. Pushes a transcript line into session.pendingMentionContexts so the
|
|
8
|
+
// coding agent sees the exchange on its next regular turn
|
|
9
|
+
// 4. Fires a notification + push for the targeted user
|
|
10
|
+
//
|
|
11
|
+
// Multi-target (`@a @b`) is intentionally NOT supported here -- callers send
|
|
12
|
+
// one user_mention per target. This keeps the wire protocol and progress
|
|
13
|
+
// tracking simple. Multi-target support can be layered on later.
|
|
14
|
+
|
|
15
|
+
function attachUserMention(ctx) {
|
|
16
|
+
var slug = ctx.slug || "";
|
|
17
|
+
var sm = ctx.sm;
|
|
18
|
+
var send = ctx.send;
|
|
19
|
+
var sendTo = ctx.sendTo;
|
|
20
|
+
var sendToSession = ctx.sendToSession;
|
|
21
|
+
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
22
|
+
var getSessionForWs = ctx.getSessionForWs;
|
|
23
|
+
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
24
|
+
var saveImageFile = ctx.saveImageFile;
|
|
25
|
+
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
26
|
+
var usersModule = ctx.usersModule;
|
|
27
|
+
var pushModule = ctx.pushModule || null;
|
|
28
|
+
var isUserOnline = ctx.isUserOnline || function () { return false; };
|
|
29
|
+
var getNotificationsModule = ctx.getNotificationsModule || function () { return null; };
|
|
30
|
+
var getProjectTitle = ctx.getProjectTitle || function () { return slug; };
|
|
31
|
+
|
|
32
|
+
function buildPreview(text) {
|
|
33
|
+
var preview = (text || "").replace(/\s+/g, " ").trim();
|
|
34
|
+
if (preview.length > 140) preview = preview.substring(0, 140) + "...";
|
|
35
|
+
return preview;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveSenderName(ws) {
|
|
39
|
+
if (!ws._clayUser) return "Someone";
|
|
40
|
+
var u = ws._clayUser;
|
|
41
|
+
var p = u.profile || {};
|
|
42
|
+
return p.name || u.displayName || u.username || "Someone";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveSenderAvatar(ws) {
|
|
46
|
+
if (!ws._clayUser) return null;
|
|
47
|
+
var u = ws._clayUser;
|
|
48
|
+
var p = u.profile || {};
|
|
49
|
+
return {
|
|
50
|
+
style: p.avatarStyle || "thumbs",
|
|
51
|
+
seed: p.avatarSeed || u.username || u.id,
|
|
52
|
+
color: p.avatarColor || "#7c3aed",
|
|
53
|
+
custom: p.avatarCustom || "",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveTargetUser(targetUserId) {
|
|
58
|
+
if (!targetUserId || !usersModule || typeof usersModule.findUserById !== "function") return null;
|
|
59
|
+
var u = usersModule.findUserById(targetUserId);
|
|
60
|
+
if (!u) return null;
|
|
61
|
+
var p = u.profile || {};
|
|
62
|
+
return {
|
|
63
|
+
id: u.id,
|
|
64
|
+
name: p.name || u.displayName || u.username || "User",
|
|
65
|
+
username: u.username,
|
|
66
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
67
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
68
|
+
avatarColor: p.avatarColor || "#7c3aed",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleUserMention(ws, msg) {
|
|
73
|
+
if (!msg.targetUserId) {
|
|
74
|
+
sendTo(ws, { type: "user_mention_error", error: "Missing targetUserId" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
var session = getSessionForWs(ws);
|
|
81
|
+
if (!session) return;
|
|
82
|
+
|
|
83
|
+
if (!ws._clayUser) {
|
|
84
|
+
sendTo(ws, { type: "user_mention_error", error: "You must be signed in to mention another user." });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (ws._clayUser.id === msg.targetUserId) {
|
|
88
|
+
sendTo(ws, { type: "user_mention_error", error: "You cannot mention yourself." });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var target = resolveTargetUser(msg.targetUserId);
|
|
93
|
+
if (!target) {
|
|
94
|
+
sendTo(ws, { type: "user_mention_error", error: "Target user not found." });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var fromId = ws._clayUser.id;
|
|
99
|
+
var fromName = resolveSenderName(ws);
|
|
100
|
+
var fromAvatar = resolveSenderAvatar(ws) || {};
|
|
101
|
+
|
|
102
|
+
// Save images to disk (same pattern as regular and mate-mention messages)
|
|
103
|
+
var imageRefs = [];
|
|
104
|
+
if (msg.images && msg.images.length > 0) {
|
|
105
|
+
for (var i = 0; i < msg.images.length; i++) {
|
|
106
|
+
var img = msg.images[i];
|
|
107
|
+
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
108
|
+
if (savedName) {
|
|
109
|
+
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var entry = {
|
|
115
|
+
type: "user_mention",
|
|
116
|
+
from: fromId,
|
|
117
|
+
fromName: fromName,
|
|
118
|
+
targetUserId: target.id,
|
|
119
|
+
targetName: target.name,
|
|
120
|
+
targetUsername: target.username,
|
|
121
|
+
targetAvatarStyle: target.avatarStyle,
|
|
122
|
+
targetAvatarSeed: target.avatarSeed,
|
|
123
|
+
targetAvatarColor: target.avatarColor,
|
|
124
|
+
text: msg.text || "",
|
|
125
|
+
_ts: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
if (msg.pastes && msg.pastes.length > 0) entry.pastes = msg.pastes;
|
|
128
|
+
if (imageRefs.length > 0) entry.imageRefs = imageRefs;
|
|
129
|
+
|
|
130
|
+
session.history.push(entry);
|
|
131
|
+
sm.appendToSessionFile(session, entry);
|
|
132
|
+
|
|
133
|
+
// Hydrate image refs for live broadcast (clients want urls, not file names).
|
|
134
|
+
// Use sendToSessionOthers so the sender's tab renders the message locally
|
|
135
|
+
// (via input.js) without duplicating it from the WS echo.
|
|
136
|
+
var live = hydrateImageRefs ? hydrateImageRefs(entry) : entry;
|
|
137
|
+
sendToSessionOthers(ws, session.localId, live);
|
|
138
|
+
|
|
139
|
+
// Queue side-channel transcript for the coding agent's next turn so the
|
|
140
|
+
// agent sees what the humans discussed. Mirrors how mate mentions inject.
|
|
141
|
+
if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
|
|
142
|
+
session.pendingMentionContexts.push(
|
|
143
|
+
"[Context: @" + fromName + " mentioned @" + target.name + " in a side conversation]\n" +
|
|
144
|
+
fromName + " to @" + target.name + ": " + (msg.text || "") + "\n" +
|
|
145
|
+
"[End of @mention context. This is for your reference only. Do not respond to it directly.]"
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Notification for the mentioned user
|
|
149
|
+
var preview = buildPreview(msg.text);
|
|
150
|
+
var notificationsModule = getNotificationsModule();
|
|
151
|
+
if (notificationsModule && typeof notificationsModule.notify === "function") {
|
|
152
|
+
notificationsModule.notify("user_mention", {
|
|
153
|
+
slug: slug,
|
|
154
|
+
sessionId: session.localId,
|
|
155
|
+
ownerId: session.ownerId || null,
|
|
156
|
+
targetUserId: target.id,
|
|
157
|
+
fromUserId: fromId,
|
|
158
|
+
fromName: fromName,
|
|
159
|
+
fromAvatarStyle: fromAvatar.style || null,
|
|
160
|
+
fromAvatarSeed: fromAvatar.seed || null,
|
|
161
|
+
fromAvatarColor: fromAvatar.color || null,
|
|
162
|
+
fromAvatarCustom: fromAvatar.custom || "",
|
|
163
|
+
preview: preview || "Mentioned you",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Push notification: only if the target is offline. If they are online,
|
|
168
|
+
// the notification banner already handles in-app delivery. This prevents
|
|
169
|
+
// double-buzzing for users who are actively connected.
|
|
170
|
+
if (pushModule && typeof pushModule.sendPushToUser === "function" && !isUserOnline(target.id)) {
|
|
171
|
+
try {
|
|
172
|
+
pushModule.sendPushToUser(target.id, {
|
|
173
|
+
type: "user_mention",
|
|
174
|
+
title: "@" + fromName + " mentioned you",
|
|
175
|
+
body: preview || "Tap to open the session",
|
|
176
|
+
tag: "clay-user-mention-" + target.id,
|
|
177
|
+
data: {
|
|
178
|
+
slug: slug,
|
|
179
|
+
sessionId: session.localId,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error("[user-mention] push failed:", e && e.message ? e.message : e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
handleUserMention: handleUserMention,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { attachUserMention: attachUserMention };
|
package/lib/project.js
CHANGED
|
@@ -17,6 +17,7 @@ var userPresence = require("./user-presence");
|
|
|
17
17
|
var { attachDebate } = require("./project-debate");
|
|
18
18
|
var { attachMemory } = require("./project-memory");
|
|
19
19
|
var { attachMateInteraction } = require("./project-mate-interaction");
|
|
20
|
+
var { attachUserMention } = require("./project-user-mention");
|
|
20
21
|
var { attachLoop } = require("./project-loop");
|
|
21
22
|
var { attachFileWatch } = require("./project-file-watch");
|
|
22
23
|
var { attachHTTP } = require("./project-http");
|
|
@@ -848,6 +849,12 @@ function createProjectContext(opts) {
|
|
|
848
849
|
return;
|
|
849
850
|
}
|
|
850
851
|
|
|
852
|
+
// --- @Mention: user-to-user side conversation in this session ---
|
|
853
|
+
if (msg.type === "user_mention") {
|
|
854
|
+
handleUserMention(ws, msg);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
851
858
|
if (msg.type === "mention_stop") {
|
|
852
859
|
var session = getSessionForWs(ws);
|
|
853
860
|
if (session && session._mentionInProgress) {
|
|
@@ -1046,6 +1053,26 @@ function createProjectContext(opts) {
|
|
|
1046
1053
|
var digestDmTurn = _mateInteraction.digestDmTurn;
|
|
1047
1054
|
var enqueueDigest = _mateInteraction.enqueueDigest;
|
|
1048
1055
|
|
|
1056
|
+
// --- User-to-user mention engine (delegated to project-user-mention.js) ---
|
|
1057
|
+
var _userMention = attachUserMention({
|
|
1058
|
+
slug: slug,
|
|
1059
|
+
sm: sm,
|
|
1060
|
+
send: send,
|
|
1061
|
+
sendTo: sendTo,
|
|
1062
|
+
sendToSession: sendToSession,
|
|
1063
|
+
sendToSessionOthers: sendToSessionOthers,
|
|
1064
|
+
getSessionForWs: getSessionForWs,
|
|
1065
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
1066
|
+
saveImageFile: saveImageFile,
|
|
1067
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
1068
|
+
usersModule: usersModule,
|
|
1069
|
+
pushModule: pushModule,
|
|
1070
|
+
isUserOnline: opts.isUserOnline || function () { return false; },
|
|
1071
|
+
getNotificationsModule: function () { return _notifications; },
|
|
1072
|
+
getProjectTitle: function () { return title || slug; },
|
|
1073
|
+
});
|
|
1074
|
+
var handleUserMention = _userMention.handleUserMention;
|
|
1075
|
+
|
|
1049
1076
|
// --- Debate engine (delegated to project-debate.js) ---
|
|
1050
1077
|
var _debate = attachDebate({
|
|
1051
1078
|
cwd: cwd,
|
package/lib/public/app.js
CHANGED
|
@@ -730,6 +730,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
730
730
|
getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
|
|
731
731
|
showMatePreThinking: function () { showMatePreThinking(); },
|
|
732
732
|
showClaudePreThinking: function () { showClaudePreThinking(); },
|
|
733
|
+
myUserId: function () { return store.get('myUserId'); },
|
|
734
|
+
myDisplayName: function () {
|
|
735
|
+
var u = null;
|
|
736
|
+
try { u = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch (e) {}
|
|
737
|
+
return (u && (u.displayName || u.username)) || document.body.dataset.myDisplayName || "Me";
|
|
738
|
+
},
|
|
733
739
|
});
|
|
734
740
|
|
|
735
741
|
// --- @Mention module ---
|
|
@@ -740,6 +746,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
740
746
|
messagesEl: messagesEl,
|
|
741
747
|
matesList: function () { return store.get('cachedMatesList') || []; },
|
|
742
748
|
availableBuiltins: function () { return store.get('cachedAvailableBuiltins') || []; },
|
|
749
|
+
allUsers: function () { return store.get('cachedAllUsers') || []; },
|
|
750
|
+
myUserId: function () { return store.get('myUserId'); },
|
|
743
751
|
scrollToBottom: scrollToBottom,
|
|
744
752
|
addUserMessage: addUserMessage,
|
|
745
753
|
addCopyHandler: addCopyHandler,
|
|
@@ -53,7 +53,7 @@ import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitD
|
|
|
53
53
|
import { handleSkillInstallWs } from './app-skills-install.js';
|
|
54
54
|
import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner } from './app-notifications.js';
|
|
55
55
|
import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
|
|
56
|
-
import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse } from './mention.js';
|
|
56
|
+
import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse, renderUserMention } from './mention.js';
|
|
57
57
|
|
|
58
58
|
// --- DOM refs (cached once, stable for page lifetime) ---
|
|
59
59
|
var messagesEl = document.getElementById("messages");
|
|
@@ -1434,6 +1434,18 @@ export function processMessage(msg) {
|
|
|
1434
1434
|
renderMentionResponse(msg);
|
|
1435
1435
|
break;
|
|
1436
1436
|
|
|
1437
|
+
case "user_mention":
|
|
1438
|
+
// User-to-user side conversation entry. Renders for any session viewer
|
|
1439
|
+
// (sender's other tabs and the mentioned user, if they are watching the
|
|
1440
|
+
// session). On the sender's own tab, the server uses sendToSessionOthers
|
|
1441
|
+
// so we never get a duplicate here.
|
|
1442
|
+
finalizeAssistantBlock();
|
|
1443
|
+
renderUserMention(msg);
|
|
1444
|
+
break;
|
|
1445
|
+
case "user_mention_error":
|
|
1446
|
+
if (msg.error) showToast("@Mention: " + msg.error, "error");
|
|
1447
|
+
break;
|
|
1448
|
+
|
|
1437
1449
|
// --- Debate ---
|
|
1438
1450
|
case "debate_preparing":
|
|
1439
1451
|
if (!store.get('replayingHistory')) showDebateSticky("preparing", msg);
|
|
@@ -9,7 +9,7 @@ import { getWs } from './ws-ref.js';
|
|
|
9
9
|
import { openDm } from './app-dm.js';
|
|
10
10
|
import { getCachedProjects } from './app-projects.js';
|
|
11
11
|
import { switchProject } from './app-projects.js';
|
|
12
|
-
import { mateAvatarUrl } from './avatar.js';
|
|
12
|
+
import { mateAvatarUrl, userAvatarUrl } from './avatar.js';
|
|
13
13
|
import { openTerminal } from './terminal.js';
|
|
14
14
|
var notifications = [];
|
|
15
15
|
var unreadCount = 0;
|
|
@@ -87,16 +87,20 @@ function showBanner(notif, autoDismissMs) {
|
|
|
87
87
|
var isEmpty = notif.id === "_empty";
|
|
88
88
|
var isPermission = notif.type === "permission_request" && notif.meta && notif.meta.requestId;
|
|
89
89
|
var isAuthRequired = notif.type === "auth_required";
|
|
90
|
+
var isUserMention = notif.type === "user_mention";
|
|
90
91
|
var projectIcon = isEmpty ? null : getProjectIcon(notif.slug);
|
|
91
92
|
var projectName = isEmpty ? "" : (isAuthRequired ? "CLAY" : getProjectName(notif.slug));
|
|
92
93
|
var mate = isEmpty ? null : getMateForNotification(notif);
|
|
94
|
+
var userMentionAvatarSrc = isUserMention ? buildUserMentionAvatarSrc(notif) : null;
|
|
93
95
|
|
|
94
96
|
var banner = document.createElement("div");
|
|
95
|
-
banner.className = "notif-banner" + (isAuthRequired ? " notif-banner-update notif-banner-auth" : isPermission ? " notif-banner-permission" : "");
|
|
97
|
+
banner.className = "notif-banner" + (isAuthRequired ? " notif-banner-update notif-banner-auth" : isPermission ? " notif-banner-permission" : isUserMention ? " notif-banner-user-mention" : "");
|
|
96
98
|
if (!isEmpty) banner.setAttribute("data-notif-id", notif.id);
|
|
97
99
|
if (isAuthRequired) banner.setAttribute("data-auth-banner", "true");
|
|
98
100
|
|
|
99
|
-
var iconHtmlStr =
|
|
101
|
+
var iconHtmlStr = userMentionAvatarSrc
|
|
102
|
+
? '<img class="notif-banner-avatar" src="' + escapeHtml(userMentionAvatarSrc) + '" alt="' + escapeHtml((notif.meta && notif.meta.fromName) || "User") + '">'
|
|
103
|
+
: mate
|
|
100
104
|
? '<img class="notif-banner-avatar" src="' + escapeHtml(mateAvatarUrl(mate, 32)) + '" alt="' + escapeHtml(mate.displayName || mate.name || "Mate") + '">'
|
|
101
105
|
: isAuthRequired
|
|
102
106
|
? '<img src="/icon-banded-76.png" width="32" height="32" alt="Clay" style="border-radius:8px">'
|
|
@@ -428,7 +432,15 @@ export function handleNotificationCreated(msg) {
|
|
|
428
432
|
unreadCount = msg.unreadCount;
|
|
429
433
|
updateBadge();
|
|
430
434
|
|
|
431
|
-
|
|
435
|
+
// user_mention banners are persistent (no auto-dismiss). They go away when:
|
|
436
|
+
// 1. The user clicks the banner (which navigates to the session — see
|
|
437
|
+
// banner click handler in showBanner / navigateToNotification), OR
|
|
438
|
+
// 2. The user navigates to the target session by some other path, in which
|
|
439
|
+
// case handleNotificationCreated above already dismissed the notif on
|
|
440
|
+
// activeSessionId match, OR
|
|
441
|
+
// 3. The user clicks the X close button.
|
|
442
|
+
// Permission and auth banners are also persistent for the same UX reason.
|
|
443
|
+
var _autoDismiss = (notif.type === "permission_request" || notif.type === "auth_required" || notif.type === "user_mention") ? false : true;
|
|
432
444
|
showBanner(notif, _autoDismiss);
|
|
433
445
|
}
|
|
434
446
|
|
|
@@ -506,6 +518,30 @@ function deriveMateIdFromNotification(notif) {
|
|
|
506
518
|
return null;
|
|
507
519
|
}
|
|
508
520
|
|
|
521
|
+
// Build the sender avatar URL for a user_mention notification banner.
|
|
522
|
+
// Prefers the avatar fields stashed in `notif.meta` (set by the server when the
|
|
523
|
+
// notification is created), and falls back to the cached user list from
|
|
524
|
+
// `cachedAllUsers` so the banner still has a face even if meta is missing.
|
|
525
|
+
function buildUserMentionAvatarSrc(notif) {
|
|
526
|
+
var meta = (notif && notif.meta) || {};
|
|
527
|
+
if (meta.fromAvatarStyle || meta.fromAvatarSeed || meta.fromAvatarCustom) {
|
|
528
|
+
return userAvatarUrl({
|
|
529
|
+
avatarStyle: meta.fromAvatarStyle,
|
|
530
|
+
avatarSeed: meta.fromAvatarSeed,
|
|
531
|
+
avatarCustom: meta.fromAvatarCustom || "",
|
|
532
|
+
username: meta.fromName,
|
|
533
|
+
id: meta.fromUserId,
|
|
534
|
+
}, 32);
|
|
535
|
+
}
|
|
536
|
+
if (meta.fromUserId) {
|
|
537
|
+
var users = store.get('cachedAllUsers') || [];
|
|
538
|
+
for (var i = 0; i < users.length; i++) {
|
|
539
|
+
if (users[i] && users[i].id === meta.fromUserId) return userAvatarUrl(users[i], 32);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
509
545
|
function getMateForNotification(notif) {
|
|
510
546
|
var mateId = notif && notif.meta ? notif.meta.avatarMateId : null;
|
|
511
547
|
if (!mateId) mateId = deriveMateIdFromNotification(notif);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { setRewindMode, isRewindMode } from './rewind.js';
|
|
3
3
|
import { renderPicker as renderContextPicker } from './context-sources.js';
|
|
4
|
-
import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, renderMentionUser, removeMentionChip } from './mention.js';
|
|
4
|
+
import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, sendUserMention, renderMentionUser, renderUserMention, removeMentionChip } from './mention.js';
|
|
5
5
|
import { store } from './store.js';
|
|
6
6
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
7
|
|
|
@@ -152,9 +152,44 @@ export function sendMessage() {
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// Check for @mention: if a mate was selected, route to mention handler
|
|
156
|
-
// Exception: if we're in a DM with the same mate, send as regular message instead
|
|
155
|
+
// Check for @mention: if a mate or user was selected, route to mention handler.
|
|
156
|
+
// Exception: if we're in a DM with the same mate, send as regular message instead.
|
|
157
|
+
// (User mentions never short-circuit to DM mode; user-to-user side conversations
|
|
158
|
+
// are scoped to the active session, not pulled into DMs.)
|
|
157
159
|
var mention = parseMentionFromInput(text);
|
|
160
|
+
if (mention && mention.kind === "user") {
|
|
161
|
+
hideMentionMenu();
|
|
162
|
+
if (ctx.hideSuggestionChips) ctx.hideSuggestionChips();
|
|
163
|
+
var uMentionImages = pendingImages.slice();
|
|
164
|
+
var uMentionPastes = pendingPastes.map(function (p) { return p.text; });
|
|
165
|
+
var uMentionFiles = pendingFiles.slice();
|
|
166
|
+
var uMentionText = mention.text;
|
|
167
|
+
if (uMentionFiles.length > 0) {
|
|
168
|
+
var uFilePaths = uMentionFiles.map(function (f) { return "[Uploaded file: " + f.path + "]"; }).join("\n");
|
|
169
|
+
uMentionText = uMentionText ? uFilePaths + "\n\n" + uMentionText : uFilePaths;
|
|
170
|
+
}
|
|
171
|
+
// Optimistic local render so the sender sees their own message immediately.
|
|
172
|
+
// The server uses sendToSessionOthers, so this tab does not get a duplicate echo.
|
|
173
|
+
var myUserId = ctx.myUserId ? ctx.myUserId() : null;
|
|
174
|
+
var myDisplayName = ctx.myDisplayName ? ctx.myDisplayName() : "Me";
|
|
175
|
+
renderUserMention({
|
|
176
|
+
from: myUserId,
|
|
177
|
+
fromName: myDisplayName,
|
|
178
|
+
targetUserId: mention.userId,
|
|
179
|
+
targetName: mention.mateName,
|
|
180
|
+
text: uMentionText,
|
|
181
|
+
images: uMentionImages.length > 0 ? uMentionImages : null,
|
|
182
|
+
pastes: uMentionPastes.length > 0 ? uMentionPastes : null,
|
|
183
|
+
});
|
|
184
|
+
sendUserMention(mention.userId, uMentionText, uMentionPastes, uMentionImages);
|
|
185
|
+
ctx.inputEl.value = "";
|
|
186
|
+
stickyReapplyMention();
|
|
187
|
+
sendInputSync();
|
|
188
|
+
clearPendingImages();
|
|
189
|
+
autoResize();
|
|
190
|
+
ctx.inputEl.focus();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
158
193
|
if (mention) {
|
|
159
194
|
var dmMateId = ctx.getDmMateId ? ctx.getDmMateId() : null;
|
|
160
195
|
if (dmMateId && dmMateId === mention.mateId) {
|