clay-server 2.17.0 → 2.18.0-beta.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/lib/mates.js +85 -6
- package/lib/project.js +339 -2
- package/lib/public/app.js +50 -14
- package/lib/public/css/base.css +1 -0
- package/lib/public/css/input.css +5 -0
- package/lib/public/css/mates.css +3 -8
- package/lib/public/css/mention.css +226 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/index.html +1 -0
- package/lib/public/modules/input.js +40 -0
- package/lib/public/modules/mention.js +627 -0
- package/lib/public/modules/notifications.js +9 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +185 -0
- package/lib/sessions.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { mateAvatarUrl } from './avatar.js';
|
|
2
|
+
import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
|
|
3
|
+
import { escapeHtml } from './utils.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
|
|
6
|
+
var ctx;
|
|
7
|
+
|
|
8
|
+
// --- State ---
|
|
9
|
+
var mentionActive = false; // @ autocomplete is visible
|
|
10
|
+
var mentionAtIdx = -1; // position of the @ in input
|
|
11
|
+
var mentionFiltered = []; // filtered mate list
|
|
12
|
+
var mentionActiveIdx = -1; // highlighted item in dropdown
|
|
13
|
+
var selectedMateId = null; // selected mate for pending send
|
|
14
|
+
var selectedMateName = null; // display name of selected mate
|
|
15
|
+
|
|
16
|
+
// Streaming state
|
|
17
|
+
var currentMentionEl = null; // current mention response DOM element
|
|
18
|
+
var mentionFullText = ""; // accumulated response text
|
|
19
|
+
var mentionStreamBuffer = ""; // stream smoothing buffer
|
|
20
|
+
var mentionDrainTimer = null;
|
|
21
|
+
var activeMentionMeta = null; // { mateId, mateName, avatarColor, avatarStyle, avatarSeed } for reconnect
|
|
22
|
+
|
|
23
|
+
// --- Init ---
|
|
24
|
+
export function initMention(_ctx) {
|
|
25
|
+
ctx = _ctx;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- @ detection ---
|
|
29
|
+
// Called from input.js on each input event.
|
|
30
|
+
// Returns { active, query, startIdx } if @ mention is being typed.
|
|
31
|
+
export function checkForMention(value, cursorPos) {
|
|
32
|
+
// Look backwards from cursor to find an unmatched @
|
|
33
|
+
var i = cursorPos - 1;
|
|
34
|
+
while (i >= 0) {
|
|
35
|
+
var ch = value.charAt(i);
|
|
36
|
+
if (ch === "@") {
|
|
37
|
+
// @ must be at start of input or preceded by whitespace
|
|
38
|
+
if (i === 0 || /\s/.test(value.charAt(i - 1))) {
|
|
39
|
+
var query = value.substring(i + 1, cursorPos);
|
|
40
|
+
// Don't activate if query contains whitespace (user moved past mention)
|
|
41
|
+
if (/\s/.test(query)) break;
|
|
42
|
+
return { active: true, query: query, startIdx: i };
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (/\s/.test(ch)) break; // whitespace before finding @ means no mention
|
|
47
|
+
i--;
|
|
48
|
+
}
|
|
49
|
+
return { active: false, query: "", startIdx: -1 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Autocomplete dropdown ---
|
|
53
|
+
export function showMentionMenu(query) {
|
|
54
|
+
var mates = ctx.matesList ? ctx.matesList() : [];
|
|
55
|
+
if (!mates || mates.length === 0) {
|
|
56
|
+
hideMentionMenu();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var lowerQuery = query.toLowerCase();
|
|
61
|
+
mentionFiltered = mates.filter(function (m) {
|
|
62
|
+
if (m.status === "interviewing") return false;
|
|
63
|
+
var name = ((m.profile && m.profile.displayName) || m.name || "").toLowerCase();
|
|
64
|
+
return name.indexOf(lowerQuery) !== -1;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (mentionFiltered.length === 0) {
|
|
68
|
+
hideMentionMenu();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mentionActive = true;
|
|
73
|
+
mentionActiveIdx = 0;
|
|
74
|
+
|
|
75
|
+
var menuEl = document.getElementById("mention-menu");
|
|
76
|
+
if (!menuEl) return;
|
|
77
|
+
|
|
78
|
+
menuEl.innerHTML = mentionFiltered.map(function (m, i) {
|
|
79
|
+
var name = (m.profile && m.profile.displayName) || m.name || "Mate";
|
|
80
|
+
var color = (m.profile && m.profile.avatarColor) || "#6c5ce7";
|
|
81
|
+
var avatarSrc = mateAvatarUrl(m, 24);
|
|
82
|
+
return '<div class="mention-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">' +
|
|
83
|
+
'<img class="mention-item-avatar" src="' + escapeHtml(avatarSrc) + '" width="24" height="24" />' +
|
|
84
|
+
'<span class="mention-item-name">' + escapeHtml(name) + '</span>' +
|
|
85
|
+
'<span class="mention-item-dot" style="background:' + escapeHtml(color) + '"></span>' +
|
|
86
|
+
'</div>';
|
|
87
|
+
}).join("");
|
|
88
|
+
menuEl.classList.add("visible");
|
|
89
|
+
|
|
90
|
+
menuEl.querySelectorAll(".mention-item").forEach(function (el) {
|
|
91
|
+
el.addEventListener("click", function (e) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
selectMentionItem(parseInt(el.dataset.idx));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function hideMentionMenu() {
|
|
100
|
+
mentionActive = false;
|
|
101
|
+
mentionActiveIdx = -1;
|
|
102
|
+
mentionFiltered = [];
|
|
103
|
+
var menuEl = document.getElementById("mention-menu");
|
|
104
|
+
if (menuEl) {
|
|
105
|
+
menuEl.classList.remove("visible");
|
|
106
|
+
menuEl.innerHTML = "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isMentionMenuVisible() {
|
|
111
|
+
return mentionActive && mentionFiltered.length > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function mentionMenuKeydown(e) {
|
|
115
|
+
if (!mentionActive || mentionFiltered.length === 0) return false;
|
|
116
|
+
|
|
117
|
+
if (e.key === "ArrowDown") {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
mentionActiveIdx = (mentionActiveIdx + 1) % mentionFiltered.length;
|
|
120
|
+
updateMentionHighlight();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (e.key === "ArrowUp") {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
mentionActiveIdx = (mentionActiveIdx - 1 + mentionFiltered.length) % mentionFiltered.length;
|
|
126
|
+
updateMentionHighlight();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
selectMentionItem(mentionActiveIdx);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (e.key === "Escape") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
hideMentionMenu();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function selectMentionItem(idx) {
|
|
143
|
+
if (idx < 0 || idx >= mentionFiltered.length) return;
|
|
144
|
+
var mate = mentionFiltered[idx];
|
|
145
|
+
var name = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
|
|
146
|
+
var color = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
|
|
147
|
+
var avatarSrc = mateAvatarUrl(mate, 20);
|
|
148
|
+
|
|
149
|
+
selectedMateId = mate.id;
|
|
150
|
+
selectedMateName = name;
|
|
151
|
+
|
|
152
|
+
// Remove the @query text from the textarea, keep remaining text
|
|
153
|
+
if (ctx.inputEl && mentionAtIdx >= 0) {
|
|
154
|
+
var val = ctx.inputEl.value;
|
|
155
|
+
var cursorPos = ctx.inputEl.selectionStart;
|
|
156
|
+
var before = val.substring(0, mentionAtIdx);
|
|
157
|
+
var after = val.substring(cursorPos);
|
|
158
|
+
ctx.inputEl.value = (before + after).trim();
|
|
159
|
+
ctx.inputEl.selectionStart = ctx.inputEl.selectionEnd = 0;
|
|
160
|
+
ctx.inputEl.focus();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Show visual chip in input area
|
|
164
|
+
showInputMentionChip(name, color, avatarSrc);
|
|
165
|
+
|
|
166
|
+
hideMentionMenu();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function showInputMentionChip(name, color, avatarSrc) {
|
|
170
|
+
removeInputMentionChip();
|
|
171
|
+
var chip = document.createElement("div");
|
|
172
|
+
chip.id = "input-mention-chip";
|
|
173
|
+
chip.innerHTML =
|
|
174
|
+
'<img class="input-mention-chip-avatar" src="' + escapeHtml(avatarSrc) + '" width="18" height="18" />' +
|
|
175
|
+
'<span class="input-mention-chip-name" style="color:' + escapeHtml(color) + '">@' + escapeHtml(name) + '</span>' +
|
|
176
|
+
'<button class="input-mention-chip-remove" type="button" aria-label="Remove mention">×</button>';
|
|
177
|
+
chip.style.setProperty("--chip-color", color);
|
|
178
|
+
|
|
179
|
+
// Insert before the textarea inside input-row
|
|
180
|
+
var inputRow = document.getElementById("input-row");
|
|
181
|
+
if (inputRow && ctx.inputEl) {
|
|
182
|
+
inputRow.insertBefore(chip, ctx.inputEl);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
chip.querySelector(".input-mention-chip-remove").addEventListener("click", function (e) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
removeMentionChip();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function removeInputMentionChip() {
|
|
193
|
+
var existing = document.getElementById("input-mention-chip");
|
|
194
|
+
if (existing) existing.remove();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function removeMentionChip() {
|
|
198
|
+
removeInputMentionChip();
|
|
199
|
+
selectedMateId = null;
|
|
200
|
+
selectedMateName = null;
|
|
201
|
+
if (ctx.inputEl) ctx.inputEl.focus();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateMentionHighlight() {
|
|
205
|
+
var menuEl = document.getElementById("mention-menu");
|
|
206
|
+
if (!menuEl) return;
|
|
207
|
+
menuEl.querySelectorAll(".mention-item").forEach(function (el, i) {
|
|
208
|
+
el.classList.toggle("active", i === mentionActiveIdx);
|
|
209
|
+
});
|
|
210
|
+
var activeEl = menuEl.querySelector(".mention-item.active");
|
|
211
|
+
if (activeEl) activeEl.scrollIntoView({ block: "nearest" });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Store the @ position when check detects mention
|
|
215
|
+
export function setMentionAtIdx(idx) {
|
|
216
|
+
mentionAtIdx = idx;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Mention send ---
|
|
220
|
+
// Returns { mateId, mateName, text } if input has an @mention, or null
|
|
221
|
+
export function parseMentionFromInput(text) {
|
|
222
|
+
if (!selectedMateId || !selectedMateName) return null;
|
|
223
|
+
// The chip is shown separately; textarea contains only the message text
|
|
224
|
+
var mentionText = text.trim();
|
|
225
|
+
if (!mentionText) return null;
|
|
226
|
+
return { mateId: selectedMateId, mateName: selectedMateName, text: mentionText };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function clearMentionState() {
|
|
230
|
+
selectedMateId = null;
|
|
231
|
+
selectedMateName = null;
|
|
232
|
+
mentionAtIdx = -1;
|
|
233
|
+
removeInputMentionChip();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function sendMention(mateId, text) {
|
|
237
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
238
|
+
ctx.ws.send(JSON.stringify({ type: "mention", mateId: mateId, text: text }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- Mention response rendering ---
|
|
242
|
+
|
|
243
|
+
// Recreate the mention block if it was lost (e.g. session switch)
|
|
244
|
+
function ensureMentionBlock() {
|
|
245
|
+
if (currentMentionEl && currentMentionEl.parentNode) return; // still in DOM
|
|
246
|
+
if (!activeMentionMeta) return;
|
|
247
|
+
// Recreate from saved meta
|
|
248
|
+
handleMentionStart(activeMentionMeta);
|
|
249
|
+
// Re-render any accumulated text
|
|
250
|
+
if (mentionFullText) {
|
|
251
|
+
var contentEl = currentMentionEl.querySelector(".mention-content");
|
|
252
|
+
if (contentEl) {
|
|
253
|
+
contentEl.innerHTML = renderMarkdown(mentionFullText);
|
|
254
|
+
highlightCodeBlocks(contentEl);
|
|
255
|
+
}
|
|
256
|
+
// Hide activity bar since we have text
|
|
257
|
+
var bar = currentMentionEl.querySelector(".mention-activity-bar");
|
|
258
|
+
if (bar) bar.style.display = "none";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function handleMentionStart(msg) {
|
|
263
|
+
// Save meta for potential reconnect after session switch
|
|
264
|
+
activeMentionMeta = {
|
|
265
|
+
mateId: msg.mateId,
|
|
266
|
+
mateName: msg.mateName,
|
|
267
|
+
avatarColor: msg.avatarColor,
|
|
268
|
+
avatarStyle: msg.avatarStyle,
|
|
269
|
+
avatarSeed: msg.avatarSeed,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
var avatarSrc = buildMentionAvatarUrl(msg);
|
|
273
|
+
|
|
274
|
+
if (isMateDm()) {
|
|
275
|
+
// Mate DM: render as DM-style assistant message
|
|
276
|
+
currentMentionEl = document.createElement("div");
|
|
277
|
+
currentMentionEl.className = "msg-assistant msg-mention-dm";
|
|
278
|
+
|
|
279
|
+
var avi = document.createElement("img");
|
|
280
|
+
avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
|
|
281
|
+
avi.src = avatarSrc;
|
|
282
|
+
currentMentionEl.appendChild(avi);
|
|
283
|
+
|
|
284
|
+
var contentWrap = document.createElement("div");
|
|
285
|
+
contentWrap.className = "dm-bubble-content";
|
|
286
|
+
|
|
287
|
+
var header = document.createElement("div");
|
|
288
|
+
header.className = "dm-bubble-header";
|
|
289
|
+
var nameSpan = document.createElement("span");
|
|
290
|
+
nameSpan.className = "dm-bubble-name";
|
|
291
|
+
nameSpan.style.color = msg.avatarColor || "#6c5ce7";
|
|
292
|
+
nameSpan.textContent = msg.mateName || "Mate";
|
|
293
|
+
header.appendChild(nameSpan);
|
|
294
|
+
|
|
295
|
+
var badge = document.createElement("span");
|
|
296
|
+
badge.className = "mention-badge";
|
|
297
|
+
badge.textContent = "@MENTION";
|
|
298
|
+
header.appendChild(badge);
|
|
299
|
+
contentWrap.appendChild(header);
|
|
300
|
+
|
|
301
|
+
// Activity indicator
|
|
302
|
+
var activityDiv = document.createElement("div");
|
|
303
|
+
activityDiv.className = "activity-inline mention-activity-bar";
|
|
304
|
+
activityDiv.innerHTML =
|
|
305
|
+
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
306
|
+
'<span class="activity-text">Thinking...</span>';
|
|
307
|
+
contentWrap.appendChild(activityDiv);
|
|
308
|
+
|
|
309
|
+
// Content area for streamed markdown
|
|
310
|
+
var contentDiv = document.createElement("div");
|
|
311
|
+
contentDiv.className = "md-content mention-content";
|
|
312
|
+
contentDiv.dir = "auto";
|
|
313
|
+
contentWrap.appendChild(contentDiv);
|
|
314
|
+
|
|
315
|
+
currentMentionEl.appendChild(contentWrap);
|
|
316
|
+
} else {
|
|
317
|
+
// Project chat: mention block style
|
|
318
|
+
currentMentionEl = document.createElement("div");
|
|
319
|
+
currentMentionEl.className = "msg-mention";
|
|
320
|
+
currentMentionEl.style.setProperty("--mention-color", msg.avatarColor || "#6c5ce7");
|
|
321
|
+
|
|
322
|
+
var header = document.createElement("div");
|
|
323
|
+
header.className = "mention-header";
|
|
324
|
+
|
|
325
|
+
var avatar = document.createElement("img");
|
|
326
|
+
avatar.className = "mention-avatar";
|
|
327
|
+
avatar.src = avatarSrc;
|
|
328
|
+
avatar.width = 20;
|
|
329
|
+
avatar.height = 20;
|
|
330
|
+
header.appendChild(avatar);
|
|
331
|
+
|
|
332
|
+
var nameSpan = document.createElement("span");
|
|
333
|
+
nameSpan.className = "mention-name";
|
|
334
|
+
nameSpan.textContent = msg.mateName || "Mate";
|
|
335
|
+
header.appendChild(nameSpan);
|
|
336
|
+
|
|
337
|
+
currentMentionEl.appendChild(header);
|
|
338
|
+
|
|
339
|
+
// Activity indicator
|
|
340
|
+
var activityDiv = document.createElement("div");
|
|
341
|
+
activityDiv.className = "activity-inline mention-activity-bar";
|
|
342
|
+
activityDiv.innerHTML =
|
|
343
|
+
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
344
|
+
'<span class="activity-text">Thinking...</span>';
|
|
345
|
+
currentMentionEl.appendChild(activityDiv);
|
|
346
|
+
|
|
347
|
+
// Content area for streamed markdown
|
|
348
|
+
var contentDiv = document.createElement("div");
|
|
349
|
+
contentDiv.className = "md-content mention-content";
|
|
350
|
+
contentDiv.dir = "auto";
|
|
351
|
+
currentMentionEl.appendChild(contentDiv);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
mentionFullText = "";
|
|
355
|
+
mentionStreamBuffer = "";
|
|
356
|
+
|
|
357
|
+
if (ctx.messagesEl) {
|
|
358
|
+
ctx.messagesEl.appendChild(currentMentionEl);
|
|
359
|
+
refreshIcons();
|
|
360
|
+
if (ctx.scrollToBottom) ctx.scrollToBottom();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function handleMentionActivity(msg) {
|
|
365
|
+
ensureMentionBlock();
|
|
366
|
+
if (!currentMentionEl) return;
|
|
367
|
+
var bar = currentMentionEl.querySelector(".mention-activity-bar");
|
|
368
|
+
if (msg.activity) {
|
|
369
|
+
// Show or update activity
|
|
370
|
+
if (!bar) {
|
|
371
|
+
bar = document.createElement("div");
|
|
372
|
+
bar.className = "activity-inline mention-activity-bar";
|
|
373
|
+
bar.innerHTML =
|
|
374
|
+
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
375
|
+
'<span class="activity-text"></span>';
|
|
376
|
+
var contentEl = currentMentionEl.querySelector(".mention-content");
|
|
377
|
+
if (contentEl) {
|
|
378
|
+
currentMentionEl.insertBefore(bar, contentEl);
|
|
379
|
+
} else {
|
|
380
|
+
currentMentionEl.appendChild(bar);
|
|
381
|
+
}
|
|
382
|
+
refreshIcons();
|
|
383
|
+
}
|
|
384
|
+
var textEl = bar.querySelector(".activity-text");
|
|
385
|
+
if (textEl) {
|
|
386
|
+
textEl.textContent = msg.activity === "thinking" ? "Thinking..." : msg.activity;
|
|
387
|
+
}
|
|
388
|
+
bar.style.display = "";
|
|
389
|
+
} else {
|
|
390
|
+
if (bar) bar.style.display = "none";
|
|
391
|
+
}
|
|
392
|
+
if (ctx.scrollToBottom) ctx.scrollToBottom();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function handleMentionStream(msg) {
|
|
396
|
+
ensureMentionBlock();
|
|
397
|
+
if (!currentMentionEl) return;
|
|
398
|
+
|
|
399
|
+
// Hide activity bar on first text delta
|
|
400
|
+
var bar = currentMentionEl.querySelector(".mention-activity-bar");
|
|
401
|
+
if (bar) bar.style.display = "none";
|
|
402
|
+
|
|
403
|
+
mentionStreamBuffer += msg.delta;
|
|
404
|
+
if (!mentionDrainTimer) {
|
|
405
|
+
mentionDrainTimer = requestAnimationFrame(drainMentionStream);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function drainMentionStream() {
|
|
410
|
+
mentionDrainTimer = null;
|
|
411
|
+
if (!currentMentionEl || mentionStreamBuffer.length === 0) return;
|
|
412
|
+
|
|
413
|
+
var len = mentionStreamBuffer.length;
|
|
414
|
+
var n;
|
|
415
|
+
if (len > 200) n = Math.ceil(len / 4);
|
|
416
|
+
else if (len > 80) n = 8;
|
|
417
|
+
else if (len > 30) n = 5;
|
|
418
|
+
else if (len > 10) n = 2;
|
|
419
|
+
else n = 1;
|
|
420
|
+
|
|
421
|
+
var chunk = mentionStreamBuffer.slice(0, n);
|
|
422
|
+
mentionStreamBuffer = mentionStreamBuffer.slice(n);
|
|
423
|
+
mentionFullText += chunk;
|
|
424
|
+
|
|
425
|
+
var contentEl = currentMentionEl.querySelector(".mention-content");
|
|
426
|
+
if (contentEl) {
|
|
427
|
+
contentEl.innerHTML = renderMarkdown(mentionFullText);
|
|
428
|
+
highlightCodeBlocks(contentEl);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (ctx.scrollToBottom) ctx.scrollToBottom();
|
|
432
|
+
|
|
433
|
+
if (mentionStreamBuffer.length > 0) {
|
|
434
|
+
mentionDrainTimer = requestAnimationFrame(drainMentionStream);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function flushMentionStream() {
|
|
439
|
+
if (mentionDrainTimer) {
|
|
440
|
+
cancelAnimationFrame(mentionDrainTimer);
|
|
441
|
+
mentionDrainTimer = null;
|
|
442
|
+
}
|
|
443
|
+
if (mentionStreamBuffer.length > 0) {
|
|
444
|
+
mentionFullText += mentionStreamBuffer;
|
|
445
|
+
mentionStreamBuffer = "";
|
|
446
|
+
}
|
|
447
|
+
if (currentMentionEl) {
|
|
448
|
+
var contentEl = currentMentionEl.querySelector(".mention-content");
|
|
449
|
+
if (contentEl) {
|
|
450
|
+
contentEl.innerHTML = renderMarkdown(mentionFullText);
|
|
451
|
+
highlightCodeBlocks(contentEl);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function handleMentionDone(msg) {
|
|
457
|
+
flushMentionStream();
|
|
458
|
+
// Hide activity bar
|
|
459
|
+
if (currentMentionEl) {
|
|
460
|
+
var bar = currentMentionEl.querySelector(".mention-activity-bar");
|
|
461
|
+
if (bar) bar.style.display = "none";
|
|
462
|
+
// Add copy handler so user can "click to grab this"
|
|
463
|
+
if (ctx.addCopyHandler && mentionFullText) {
|
|
464
|
+
ctx.addCopyHandler(currentMentionEl, mentionFullText);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
currentMentionEl = null;
|
|
468
|
+
activeMentionMeta = null;
|
|
469
|
+
mentionFullText = "";
|
|
470
|
+
if (ctx.scrollToBottom) ctx.scrollToBottom();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function handleMentionError(msg) {
|
|
474
|
+
flushMentionStream();
|
|
475
|
+
if (currentMentionEl) {
|
|
476
|
+
var bar = currentMentionEl.querySelector(".mention-activity-bar");
|
|
477
|
+
if (bar) bar.style.display = "none";
|
|
478
|
+
var contentEl = currentMentionEl.querySelector(".mention-content");
|
|
479
|
+
if (contentEl) {
|
|
480
|
+
contentEl.innerHTML = '<div class="mention-error">Error: ' + escapeHtml(msg.error || "Unknown error") + '</div>';
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
currentMentionEl = null;
|
|
484
|
+
activeMentionMeta = null;
|
|
485
|
+
mentionFullText = "";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- Helpers ---
|
|
489
|
+
function isMateDm() {
|
|
490
|
+
return document.body.classList.contains("mate-dm-active");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function timeStr() {
|
|
494
|
+
var now = new Date();
|
|
495
|
+
return String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function buildMentionAvatarUrl(meta) {
|
|
499
|
+
return "https://api.dicebear.com/7.x/" + (meta.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(meta.avatarSeed || meta.mateId);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// --- History replay: render saved mention entries ---
|
|
503
|
+
export function renderMentionUser(entry) {
|
|
504
|
+
// Render user message with @mention indicator
|
|
505
|
+
var div = document.createElement("div");
|
|
506
|
+
div.className = "msg-user";
|
|
507
|
+
|
|
508
|
+
var bubble = document.createElement("div");
|
|
509
|
+
bubble.className = "bubble";
|
|
510
|
+
bubble.dir = "auto";
|
|
511
|
+
|
|
512
|
+
var textEl = document.createElement("span");
|
|
513
|
+
textEl.innerHTML = '<span class="mention-chip">@' + escapeHtml(entry.mateName || "Mate") + '</span> ' + escapeHtml(entry.text || "");
|
|
514
|
+
bubble.appendChild(textEl);
|
|
515
|
+
|
|
516
|
+
// In Mate DM: use DM-style layout with avatar + name header
|
|
517
|
+
if (isMateDm() && document.body.dataset.myAvatarUrl) {
|
|
518
|
+
var avi = document.createElement("img");
|
|
519
|
+
avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
|
|
520
|
+
avi.src = document.body.dataset.myAvatarUrl;
|
|
521
|
+
div.appendChild(avi);
|
|
522
|
+
|
|
523
|
+
var contentWrap = document.createElement("div");
|
|
524
|
+
contentWrap.className = "dm-bubble-content";
|
|
525
|
+
|
|
526
|
+
var header = document.createElement("div");
|
|
527
|
+
header.className = "dm-bubble-header";
|
|
528
|
+
var nameSpan = document.createElement("span");
|
|
529
|
+
nameSpan.className = "dm-bubble-name";
|
|
530
|
+
nameSpan.textContent = document.body.dataset.myDisplayName || "Me";
|
|
531
|
+
header.appendChild(nameSpan);
|
|
532
|
+
var ts = document.createElement("span");
|
|
533
|
+
ts.className = "dm-bubble-time";
|
|
534
|
+
ts.textContent = timeStr();
|
|
535
|
+
header.appendChild(ts);
|
|
536
|
+
contentWrap.appendChild(header);
|
|
537
|
+
contentWrap.appendChild(bubble);
|
|
538
|
+
div.appendChild(contentWrap);
|
|
539
|
+
} else {
|
|
540
|
+
div.appendChild(bubble);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (ctx.messagesEl) ctx.messagesEl.appendChild(div);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function renderMentionResponse(entry) {
|
|
547
|
+
var avatarSrc = buildMentionAvatarUrl(entry);
|
|
548
|
+
|
|
549
|
+
// In Mate DM: render as DM-style message (like assistant messages)
|
|
550
|
+
if (isMateDm()) {
|
|
551
|
+
var el = document.createElement("div");
|
|
552
|
+
el.className = "msg-assistant msg-mention-dm";
|
|
553
|
+
|
|
554
|
+
var avi = document.createElement("img");
|
|
555
|
+
avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
|
|
556
|
+
avi.src = avatarSrc;
|
|
557
|
+
el.appendChild(avi);
|
|
558
|
+
|
|
559
|
+
var contentWrap = document.createElement("div");
|
|
560
|
+
contentWrap.className = "dm-bubble-content";
|
|
561
|
+
|
|
562
|
+
var header = document.createElement("div");
|
|
563
|
+
header.className = "dm-bubble-header";
|
|
564
|
+
var nameSpan = document.createElement("span");
|
|
565
|
+
nameSpan.className = "dm-bubble-name";
|
|
566
|
+
nameSpan.style.color = entry.avatarColor || "#6c5ce7";
|
|
567
|
+
nameSpan.textContent = entry.mateName || "Mate";
|
|
568
|
+
header.appendChild(nameSpan);
|
|
569
|
+
|
|
570
|
+
var badge = document.createElement("span");
|
|
571
|
+
badge.className = "mention-badge";
|
|
572
|
+
badge.textContent = "@MENTION";
|
|
573
|
+
header.appendChild(badge);
|
|
574
|
+
|
|
575
|
+
var ts = document.createElement("span");
|
|
576
|
+
ts.className = "dm-bubble-time";
|
|
577
|
+
ts.textContent = timeStr();
|
|
578
|
+
header.appendChild(ts);
|
|
579
|
+
contentWrap.appendChild(header);
|
|
580
|
+
|
|
581
|
+
var contentDiv = document.createElement("div");
|
|
582
|
+
contentDiv.className = "md-content mention-content";
|
|
583
|
+
contentDiv.dir = "auto";
|
|
584
|
+
contentDiv.innerHTML = renderMarkdown(entry.text || "");
|
|
585
|
+
highlightCodeBlocks(contentDiv);
|
|
586
|
+
contentWrap.appendChild(contentDiv);
|
|
587
|
+
el.appendChild(contentWrap);
|
|
588
|
+
|
|
589
|
+
if (ctx.messagesEl) ctx.messagesEl.appendChild(el);
|
|
590
|
+
} else {
|
|
591
|
+
// Project chat: use mention block style
|
|
592
|
+
var el = document.createElement("div");
|
|
593
|
+
el.className = "msg-mention";
|
|
594
|
+
el.style.setProperty("--mention-color", entry.avatarColor || "#6c5ce7");
|
|
595
|
+
|
|
596
|
+
var mheader = document.createElement("div");
|
|
597
|
+
mheader.className = "mention-header";
|
|
598
|
+
|
|
599
|
+
var avatar = document.createElement("img");
|
|
600
|
+
avatar.className = "mention-avatar";
|
|
601
|
+
avatar.src = avatarSrc;
|
|
602
|
+
avatar.width = 20;
|
|
603
|
+
avatar.height = 20;
|
|
604
|
+
mheader.appendChild(avatar);
|
|
605
|
+
|
|
606
|
+
var mname = document.createElement("span");
|
|
607
|
+
mname.className = "mention-name";
|
|
608
|
+
mname.textContent = entry.mateName || "Mate";
|
|
609
|
+
mheader.appendChild(mname);
|
|
610
|
+
|
|
611
|
+
el.appendChild(mheader);
|
|
612
|
+
|
|
613
|
+
var contentDiv = document.createElement("div");
|
|
614
|
+
contentDiv.className = "md-content mention-content";
|
|
615
|
+
contentDiv.dir = "auto";
|
|
616
|
+
contentDiv.innerHTML = renderMarkdown(entry.text || "");
|
|
617
|
+
highlightCodeBlocks(contentDiv);
|
|
618
|
+
el.appendChild(contentDiv);
|
|
619
|
+
|
|
620
|
+
if (ctx.messagesEl) ctx.messagesEl.appendChild(el);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Add copy handler
|
|
624
|
+
if (ctx.addCopyHandler && entry.text) {
|
|
625
|
+
ctx.addCopyHandler(el, entry.text);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
@@ -73,12 +73,18 @@ export function initNotifications(_ctx) {
|
|
|
73
73
|
var layout = $("layout");
|
|
74
74
|
var mobileTabBar = document.getElementById("mobile-tab-bar");
|
|
75
75
|
function onViewportChange() {
|
|
76
|
-
|
|
76
|
+
var vv = window.visualViewport;
|
|
77
|
+
// Shrink layout to visual viewport height so input area sits above keyboard
|
|
78
|
+
layout.style.height = vv.height + "px";
|
|
79
|
+
// Compensate for any vertical offset (iOS can shift the visual viewport)
|
|
80
|
+
layout.style.top = vv.offsetTop + "px";
|
|
77
81
|
document.documentElement.scrollTop = 0;
|
|
78
|
-
|
|
82
|
+
// Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
|
|
83
|
+
var keyboardOpen = vv.height < window.innerHeight - 100;
|
|
84
|
+
document.body.classList.toggle("keyboard-open", keyboardOpen);
|
|
85
|
+
if (!keyboardOpen) ctx.scrollToBottom();
|
|
79
86
|
// Hide tab bar when software keyboard is open
|
|
80
87
|
if (mobileTabBar) {
|
|
81
|
-
var keyboardOpen = window.visualViewport.height < window.innerHeight * 0.75;
|
|
82
88
|
if (keyboardOpen) {
|
|
83
89
|
mobileTabBar.classList.add("keyboard-hidden");
|
|
84
90
|
} else {
|
package/lib/public/style.css
CHANGED