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.
@@ -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">&times;</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
- layout.style.height = window.visualViewport.height + "px";
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
- ctx.scrollToBottom();
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 {
@@ -26,3 +26,4 @@
26
26
  @import url("css/tooltip.css");
27
27
  @import url("css/mates.css");
28
28
  @import url("css/command-palette.css");
29
+ @import url("css/mention.css");