clay-server 3.8.0-beta.1 → 3.8.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.
Files changed (35) hide show
  1. package/lib/builtin-mates.js +14 -14
  2. package/lib/mates.js +2 -6
  3. package/lib/project-mate-interaction.js +7 -7
  4. package/lib/project-sessions.js +1 -1
  5. package/lib/project-user-mention.js +4 -4
  6. package/lib/project.js +4 -3
  7. package/lib/public/css/avatar-imprints.css +46 -0
  8. package/lib/public/css/icon-strip.css +3 -3
  9. package/lib/public/css/profile.css +123 -95
  10. package/lib/public/css/session-actions.css +40 -9
  11. package/lib/public/css/user-settings.css +1 -1
  12. package/lib/public/index.html +2 -2
  13. package/lib/public/modules/app-cursors.js +2 -2
  14. package/lib/public/modules/app-dm.js +1 -8
  15. package/lib/public/modules/app-messages.js +2 -2
  16. package/lib/public/modules/app-projects.js +7 -6
  17. package/lib/public/modules/app-rate-limit.js +0 -1
  18. package/lib/public/modules/app-rendering.js +0 -3
  19. package/lib/public/modules/avatar-imprint.js +134 -0
  20. package/lib/public/modules/avatar.js +25 -19
  21. package/lib/public/modules/debate.js +10 -5
  22. package/lib/public/modules/mate-sidebar.js +2 -2
  23. package/lib/public/modules/mention.js +26 -16
  24. package/lib/public/modules/profile.js +73 -190
  25. package/lib/public/modules/session-actions.js +5 -3
  26. package/lib/public/modules/sidebar-mates.js +5 -4
  27. package/lib/public/modules/sidebar-sessions.js +1 -1
  28. package/lib/public/style.css +1 -0
  29. package/lib/server-auth.js +2 -2
  30. package/lib/server-dm.js +8 -8
  31. package/lib/server-mates.js +3 -3
  32. package/lib/server-settings.js +2 -2
  33. package/lib/server.js +5 -4
  34. package/lib/users.js +4 -4
  35. package/package.json +1 -1
@@ -1,31 +1,18 @@
1
- // User profile module — Discord-style popover for name, language, avatar
1
+ // User profile module — profile popovers for people and Mates
2
2
  // Stores profile server-side in ~/.clay/profile.json
3
- // Avatar generated via DiceBear API (deterministic SVG from seed)
3
+ // Generated identities use deterministic Clay Imprints.
4
4
 
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { setSTTLang } from './stt.js';
7
- import { avatarUrl, mateAvatarUrl, AVATAR_STYLES } from './avatar.js';
7
+ import { avatarUrl, mateAvatarUrl, mateMarkUrl } from './avatar.js';
8
8
  import { store } from './store.js';
9
9
  import { applyTerminalFont } from './terminal-prefs.js';
10
10
 
11
11
  var ctx;
12
- var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed', avatarCustom: '' };
12
+ var profile = { name: '', lang: 'en-US', avatarStyle: 'imprint', avatarSeed: '', avatarColor: '#5857fc', avatarCustom: '' };
13
13
  var profileUsername = '';
14
14
  var popoverEl = null;
15
15
  var saveTimer = null;
16
- var previewSeed = '';
17
-
18
- // AVATAR_STYLES imported from avatar.js
19
-
20
- var COLORS = [
21
- '#7c3aed', '#4f46e5', '#2563eb', '#0891b2',
22
- '#059669', '#65a30d', '#d97706', '#dc2626',
23
- '#db2777', '#6366f1', '#0d9488', '#ea580c',
24
- '#475569', '#1e293b', '#be123c', '#a21caf',
25
- '#0369a1', '#15803d',
26
- ];
27
-
28
- // avatarUrl imported from avatar.js
29
16
 
30
17
  function getAvatarSeed() {
31
18
  return profile.avatarSeed || 'anonymous';
@@ -68,7 +55,7 @@ export function applyToIsland() {
68
55
  var existingLetter = avatarWrap.querySelector('.user-island-avatar-letter');
69
56
  var url = profile.avatarCustom
70
57
  ? profile.avatarCustom
71
- : avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 32);
58
+ : avatarUrl(profile.avatarStyle || 'imprint', getAvatarSeed(), 32);
72
59
 
73
60
  if (existingImg) {
74
61
  existingImg.src = url;
@@ -81,6 +68,12 @@ export function applyToIsland() {
81
68
  }
82
69
 
83
70
  nameEl.textContent = displayName;
71
+ document.body.dataset.myAvatarUrl = url;
72
+ document.body.dataset.myDisplayName = displayName;
73
+ var messageAvatars = document.querySelectorAll('.dm-bubble-avatar-me, .icon-strip-me-avatar');
74
+ for (var avatarIndex = 0; avatarIndex < messageAvatars.length; avatarIndex++) {
75
+ messageAvatars[avatarIndex].src = url;
76
+ }
84
77
 
85
78
  // Show CTA if user hasn't personalized their name
86
79
  var ctaEl = document.querySelector('.user-island-cta');
@@ -104,7 +97,7 @@ export function showAvatarPositioner(img, objectUrl, onDone) {
104
97
  var offsetX = 0;
105
98
  var offsetY = 0;
106
99
 
107
- // Fit image so shorter side fills the circle
100
+ // Fit image so the shorter side fills the imprint frame.
108
101
  var baseScale = Math.max(viewSize / img.width, viewSize / img.height);
109
102
 
110
103
  function clampOffsets() {
@@ -287,22 +280,20 @@ function showPopover() {
287
280
  popoverEl.className = 'profile-popover';
288
281
 
289
282
  var displayName = profile.name || '';
290
- var currentColor = profile.avatarColor || '#7c3aed';
291
- var currentStyle = profile.avatarStyle || 'thumbs';
283
+ var currentStyle = profile.avatarStyle || 'imprint';
292
284
  var seed = getAvatarSeed();
293
- previewSeed = seed;
294
285
 
295
286
  var html = '';
296
287
 
297
288
  // Banner + close
298
- html += '<div class="profile-banner" style="background:' + currentColor + '">';
289
+ html += '<div class="profile-banner">';
299
290
  html += '<button class="profile-close-btn">&times;</button>';
300
291
  html += '</div>';
301
292
 
302
293
  // Avatar row (overlapping banner)
303
294
  html += '<div class="profile-avatar-row">';
304
295
  html += '<div class="profile-popover-avatar">';
305
- html += '<img class="profile-popover-avatar-img" src="' + (profile.avatarCustom || avatarUrl(currentStyle, seed, 80)) + '" alt="avatar">';
296
+ html += '<img class="profile-popover-avatar-img" src="' + (profile.avatarCustom || avatarUrl(currentStyle, seed, 80)) + '" alt="' + escapeAttr(displayName || 'Clay user') + '">';
306
297
  html += '</div>';
307
298
  html += '<div class="profile-name-display">' + escapeAttr(displayName || 'Awesome Clay User') + '</div>';
308
299
  html += '</div>';
@@ -316,40 +307,18 @@ function showPopover() {
316
307
  html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
317
308
  html += '</div>';
318
309
 
319
- // Avatar picker
310
+ // Clay Imprint or custom photo
320
311
  html += '<div class="profile-field">';
321
- html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
322
- html += '<div class="profile-avatar-grid">';
323
- // Upload button as first cell
324
- var uploadActive = profile.avatarCustom ? ' profile-avatar-option-active' : '';
325
- html += '<button class="profile-avatar-option profile-avatar-upload' + uploadActive + '" title="Upload photo">';
326
- if (profile.avatarCustom) {
327
- html += '<img src="' + profile.avatarCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
328
- } else {
329
- html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
330
- }
331
- html += '</button>';
332
- html += '<input type="file" id="profile-avatar-file" accept="image/*" style="display:none">';
333
- for (var j = 0; j < AVATAR_STYLES.length; j++) {
334
- var st = AVATAR_STYLES[j];
335
- var activeS = (!profile.avatarCustom && currentStyle === st.id) ? ' profile-avatar-option-active' : '';
336
- html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
337
- html += '<img src="' + avatarUrl(st.id, seed, 40) + '" alt="' + st.name + '">';
338
- html += '</button>';
339
- }
312
+ html += '<label class="profile-field-label">Identity</label>';
313
+ html += '<div class="profile-imprint-panel">';
314
+ html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated Clay Imprint"><span><strong>Clay Imprint</strong><small>Unique to this identity</small></span></div>';
315
+ html += '<div class="profile-imprint-actions">';
316
+ html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use imprint</button>';
317
+ html += '<button class="profile-regenerate-imprint" type="button" title="Generate another imprint">' + iconHtml('shuffle') + ' New imprint</button>';
318
+ html += '<button class="profile-avatar-upload' + (profile.avatarCustom ? ' is-active' : '') + '" type="button">Upload photo</button>';
340
319
  html += '</div>';
341
320
  html += '</div>';
342
-
343
- // Color
344
- html += '<div class="profile-field">';
345
- html += '<label class="profile-field-label">Color</label>';
346
- html += '<div class="profile-color-grid">';
347
- for (var k = 0; k < COLORS.length; k++) {
348
- var c = COLORS[k];
349
- var activeC = (currentColor === c) ? ' profile-color-active' : '';
350
- html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
351
- }
352
- html += '</div>';
321
+ html += '<input type="file" id="profile-avatar-file" accept="image/*" style="display:none">';
353
322
  html += '</div>';
354
323
 
355
324
  html += '</div>'; // close body
@@ -403,15 +372,11 @@ function showPopover() {
403
372
  profile.avatarCustom = data.avatar;
404
373
  applyToIsland();
405
374
  updatePopoverHeader();
406
- if (popoverEl) {
407
- popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
408
- b.classList.remove('profile-avatar-option-active');
409
- });
410
- }
411
375
  if (uploadBtn) {
412
- uploadBtn.classList.add('profile-avatar-option-active');
413
- uploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
376
+ uploadBtn.classList.add('is-active');
414
377
  }
378
+ var imprintButton = popoverEl && popoverEl.querySelector('.profile-use-imprint');
379
+ if (imprintButton) imprintButton.classList.remove('is-active');
415
380
  debouncedSave();
416
381
  }
417
382
  });
@@ -431,45 +396,25 @@ function showPopover() {
431
396
  });
432
397
  }
433
398
 
434
- // Avatar style — clicking confirms both the style and the current previewSeed
435
- popoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
436
- btn.addEventListener('click', function() {
437
- profile.avatarCustom = '';
438
- profile.avatarStyle = btn.dataset.style;
439
- profile.avatarSeed = previewSeed;
440
- applyToIsland();
441
- updatePopoverHeader();
442
- popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
443
- b.classList.remove('profile-avatar-option-active');
444
- });
445
- btn.classList.add('profile-avatar-option-active');
446
- // Reset upload button to + icon
447
- var upBtn = popoverEl.querySelector('.profile-avatar-upload');
448
- if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
449
- debouncedSave();
450
- });
451
- });
452
-
453
- // Shuffle button — only changes preview candidates, not the actual profile
454
- popoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
455
- e.stopPropagation();
456
- previewSeed = Math.random().toString(36).substring(2, 10);
457
- refreshAvatarPreviews();
399
+ popoverEl.querySelector('.profile-use-imprint').addEventListener('click', function() {
400
+ profile.avatarCustom = '';
401
+ applyToIsland();
402
+ updatePopoverHeader();
403
+ popoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
404
+ popoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
405
+ debouncedSave();
458
406
  });
459
407
 
460
- // Color swatches
461
- popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
462
- btn.addEventListener('click', function() {
463
- profile.avatarColor = btn.dataset.color;
464
- applyToIsland();
465
- var bannerEl = popoverEl.querySelector('.profile-banner');
466
- if (bannerEl) bannerEl.style.background = profile.avatarColor;
467
- popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
468
- b.classList.remove('profile-color-active');
469
- });
470
- btn.classList.add('profile-color-active');
471
- debouncedSave();
472
- });
408
+ popoverEl.querySelector('.profile-regenerate-imprint').addEventListener('click', function() {
409
+ profile.avatarCustom = '';
410
+ profile.avatarStyle = 'imprint';
411
+ profile.avatarSeed = Math.random().toString(36).substring(2, 10);
412
+ applyToIsland();
413
+ updatePopoverHeader();
414
+ refreshImprintPreview(popoverEl, profile.avatarStyle, profile.avatarSeed);
415
+ popoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
416
+ popoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
417
+ debouncedSave();
473
418
  });
474
419
 
475
420
  // Prevent clicks inside popover from closing it
@@ -497,17 +442,15 @@ function updatePopoverHeader() {
497
442
  var nd = popoverEl.querySelector('.profile-name-display');
498
443
  if (img) img.src = profile.avatarCustom
499
444
  ? profile.avatarCustom
500
- : avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 80);
445
+ : avatarUrl(profile.avatarStyle || 'imprint', getAvatarSeed(), 80);
501
446
  if (nd) nd.textContent = profile.name || 'Awesome Clay User';
502
447
  }
503
448
 
504
449
 
505
- function refreshAvatarPreviews() {
506
- if (!popoverEl) return;
507
- popoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
508
- var style = img.closest('.profile-avatar-option').dataset.style;
509
- img.src = avatarUrl(style, previewSeed, 40);
510
- });
450
+ function refreshImprintPreview(root, style, seed) {
451
+ if (!root) return;
452
+ var img = root.querySelector('.profile-imprint-preview img');
453
+ if (img) img.src = avatarUrl(style || 'imprint', seed || 'anonymous', 72);
511
454
  }
512
455
 
513
456
  function closeOnOutside(e) {
@@ -610,7 +553,6 @@ export function getProfileLang() {
610
553
  // --- Mate profile popover (reuses same UI minus language) ---
611
554
  var matePopoverEl = null;
612
555
  var mateSaveTimer = null;
613
- var matePreviewSeed = '';
614
556
 
615
557
  export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
616
558
  if (matePopoverEl) {
@@ -620,11 +562,10 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
620
562
 
621
563
  var mp = mateData.profile || {};
622
564
  var mateName = mp.displayName || mateData.name || '';
623
- var mateColor = mp.avatarColor || '#7c3aed';
624
- var mateStyle = mp.avatarStyle || 'bottts';
565
+ var mateColor = mp.avatarColor || '#5857fc';
566
+ var mateStyle = mp.avatarStyle || 'imprint';
625
567
  var mateSeed = mp.avatarSeed || mateData.id || 'mate';
626
568
  var mateCustom = mp.avatarCustom || '';
627
- matePreviewSeed = mateSeed;
628
569
 
629
570
  matePopoverEl = document.createElement('div');
630
571
  matePopoverEl.className = 'profile-popover mate-profile-popover';
@@ -632,15 +573,15 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
632
573
  var html = '';
633
574
 
634
575
  // Banner + close
635
- html += '<div class="profile-banner" style="background:' + mateColor + '">';
576
+ html += '<div class="profile-banner">';
636
577
  html += '<button class="profile-close-btn">&times;</button>';
637
578
  html += '</div>';
638
579
 
639
580
  // Avatar row
640
- var mateAvatarSrc = mateCustom ? mateCustom : avatarUrl(mateStyle, mateSeed, 80);
581
+ var mateAvatarSrc = mateCustom ? mateCustom : mateMarkUrl(mateName || 'Mate', 80);
641
582
  html += '<div class="profile-avatar-row">';
642
583
  html += '<div class="profile-popover-avatar">';
643
- html += '<img class="profile-popover-avatar-img" src="' + mateAvatarSrc + '" alt="avatar">';
584
+ html += '<img class="profile-popover-avatar-img" src="' + mateAvatarSrc + '" alt="' + escapeAttr(mateName || 'Clay Mate') + '">';
644
585
  html += '</div>';
645
586
  html += '<div class="profile-name-display">' + escapeAttr(mateName || 'New Mate') + '</div>';
646
587
  html += '</div>';
@@ -658,40 +599,17 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
658
599
  var mateAvatarLocked = mp.avatarLocked || false;
659
600
  if (!mateAvatarLocked) {
660
601
  html += '<div class="profile-field">';
661
- html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
662
- html += '<div class="profile-avatar-grid">';
663
- // Upload button as first cell
664
- var mateUploadActive = mateCustom ? ' profile-avatar-option-active' : '';
665
- html += '<button class="profile-avatar-option profile-avatar-upload' + mateUploadActive + '" title="Upload photo">';
666
- if (mateCustom) {
667
- html += '<img src="' + mateCustom + '" alt="Custom" class="profile-avatar-custom-preview">';
668
- } else {
669
- html += '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
670
- }
671
- html += '</button>';
672
- html += '<input type="file" id="mate-profile-avatar-file" accept="image/*" style="display:none">';
673
- for (var j = 0; j < AVATAR_STYLES.length; j++) {
674
- var st = AVATAR_STYLES[j];
675
- var activeS = (!mateCustom && mateStyle === st.id) ? ' profile-avatar-option-active' : '';
676
- html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
677
- html += '<img src="' + avatarUrl(st.id, mateSeed, 40) + '" alt="' + st.name + '">';
678
- html += '</button>';
679
- }
680
- html += '</div>';
602
+ html += '<label class="profile-field-label">Identity</label>';
603
+ html += '<div class="profile-imprint-panel">';
604
+ html += '<div class="profile-imprint-preview"><img src="' + mateMarkUrl(mateName || 'Mate', 72) + '" alt="Clay Mate initial"><span><strong>Mate initial</strong><small>Based on display name</small></span></div>';
605
+ html += '<div class="profile-imprint-actions profile-mate-mark-actions">';
606
+ html += '<button class="profile-use-imprint' + (!mateCustom ? ' is-active' : '') + '" type="button">Use initial</button>';
607
+ html += '<button class="profile-avatar-upload' + (mateCustom ? ' is-active' : '') + '" type="button">Upload image</button>';
681
608
  html += '</div>';
682
- }
683
-
684
- // Color
685
- html += '<div class="profile-field">';
686
- html += '<label class="profile-field-label">Color</label>';
687
- html += '<div class="profile-color-grid">';
688
- for (var k = 0; k < COLORS.length; k++) {
689
- var c = COLORS[k];
690
- var activeC = (mateColor === c) ? ' profile-color-active' : '';
691
- html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
692
- }
693
609
  html += '</div>';
610
+ html += '<input type="file" id="mate-profile-avatar-file" accept="image/*" style="display:none">';
694
611
  html += '</div>';
612
+ }
695
613
 
696
614
  html += '</div>'; // close body
697
615
 
@@ -746,7 +664,9 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
746
664
  var nd = matePopoverEl.querySelector('.profile-name-display');
747
665
  if (img) img.src = mateProfile.avatarCustom
748
666
  ? mateProfile.avatarCustom
749
- : avatarUrl(mateProfile.avatarStyle, mateProfile.avatarSeed, 80);
667
+ : mateMarkUrl(mateProfile.displayName || 'Mate', 80);
668
+ var preview = matePopoverEl.querySelector('.profile-imprint-preview img');
669
+ if (preview) preview.src = mateMarkUrl(mateProfile.displayName || 'Mate', 72);
750
670
  if (nd) nd.textContent = mateProfile.displayName || 'New Mate';
751
671
  }
752
672
 
@@ -791,15 +711,11 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
791
711
  if (data.ok) {
792
712
  mateProfile.avatarCustom = data.avatar;
793
713
  updateMatePopoverHeader();
794
- if (matePopoverEl) {
795
- matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
796
- b.classList.remove('profile-avatar-option-active');
797
- });
798
- }
799
714
  if (mateUploadBtn) {
800
- mateUploadBtn.classList.add('profile-avatar-option-active');
801
- mateUploadBtn.innerHTML = '<img src="' + data.avatar + '" alt="Custom" class="profile-avatar-custom-preview">';
715
+ mateUploadBtn.classList.add('is-active');
802
716
  }
717
+ var imprintButton = matePopoverEl && matePopoverEl.querySelector('.profile-use-imprint');
718
+ if (imprintButton) imprintButton.classList.remove('is-active');
803
719
  debouncedMateUpdate();
804
720
  }
805
721
  });
@@ -819,48 +735,15 @@ export function showMateProfilePopover(anchorEl, mateData, onUpdate) {
819
735
  });
820
736
  }
821
737
 
822
- // Avatar style
823
- matePopoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
824
- btn.addEventListener('click', function() {
738
+ if (!mateAvatarLocked) {
739
+ matePopoverEl.querySelector('.profile-use-imprint').addEventListener('click', function() {
825
740
  mateProfile.avatarCustom = '';
826
- mateProfile.avatarStyle = btn.dataset.style;
827
- mateProfile.avatarSeed = matePreviewSeed;
828
741
  updateMatePopoverHeader();
829
- matePopoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
830
- b.classList.remove('profile-avatar-option-active');
831
- });
832
- btn.classList.add('profile-avatar-option-active');
833
- // Reset upload button to + icon
834
- var upBtn = matePopoverEl.querySelector('.profile-avatar-upload');
835
- if (upBtn) upBtn.innerHTML = '<span class="profile-avatar-upload-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span>';
742
+ matePopoverEl.querySelector('.profile-use-imprint').classList.add('is-active');
743
+ matePopoverEl.querySelector('.profile-avatar-upload').classList.remove('is-active');
836
744
  debouncedMateUpdate();
837
745
  });
838
- });
839
-
840
- // Shuffle
841
- matePopoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
842
- e.stopPropagation();
843
- matePreviewSeed = Math.random().toString(36).substring(2, 10);
844
- if (!matePopoverEl) return;
845
- matePopoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
846
- var style = img.closest('.profile-avatar-option').dataset.style;
847
- img.src = avatarUrl(style, matePreviewSeed, 40);
848
- });
849
- });
850
-
851
- // Color swatches
852
- matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
853
- btn.addEventListener('click', function() {
854
- mateProfile.avatarColor = btn.dataset.color;
855
- var bannerEl = matePopoverEl.querySelector('.profile-banner');
856
- if (bannerEl) bannerEl.style.background = mateProfile.avatarColor;
857
- matePopoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
858
- b.classList.remove('profile-color-active');
859
- });
860
- btn.classList.add('profile-color-active');
861
- debouncedMateUpdate();
862
- });
863
- });
746
+ }
864
747
 
865
748
  matePopoverEl.addEventListener('click', function(e) { e.stopPropagation(); });
866
749
 
@@ -29,13 +29,14 @@ function positionMenu() {
29
29
  var rect = button.getBoundingClientRect();
30
30
  var width = menu.offsetWidth;
31
31
  menu.style.top = Math.round(rect.bottom + 6) + "px";
32
- menu.style.left = Math.max(8, Math.round(rect.right - width)) + "px";
32
+ menu.style.left = Math.max(8, Math.min(window.innerWidth - width - 8, Math.round(rect.right - width))) + "px";
33
33
  }
34
34
 
35
35
  function actionRow(icon, label, description) {
36
36
  var row = document.createElement("button");
37
37
  row.type = "button";
38
38
  row.className = "session-actions-row";
39
+ row.setAttribute("role", "menuitem");
39
40
  row.innerHTML = iconHtml(icon, "session-actions-row-icon") +
40
41
  '<span class="session-actions-row-copy"><strong></strong><span></span></span>' +
41
42
  iconHtml("chevron-right", "session-actions-row-chevron");
@@ -217,7 +218,8 @@ function showHandoffDialog(options) {
217
218
  function showMainMenu() {
218
219
  closeMenu();
219
220
  var el = menuShell("Session actions");
220
- var addWorker = actionRow("bot", "Add AI Worker", "Open a second agent beside this session.");
221
+ var addWorker = actionRow("bot", "Add AI worker", "Open a second agent beside this session.");
222
+ addWorker.classList.add("is-primary");
221
223
  addWorker.addEventListener("click", function () {
222
224
  var sessionId = store.get('activeSessionId');
223
225
  if (!sessionId) return;
@@ -228,7 +230,7 @@ function showMainMenu() {
228
230
  vendor: store.get('currentVendor') || "claude",
229
231
  });
230
232
  });
231
- var handoff = actionRow("forward", "Continue in another agent", "Carry this conversation into a new session.");
233
+ var handoff = actionRow("send", "Send context to another agent", "Start a new session with this conversation's context.");
232
234
  var unavailable = !store.get('sessionHasHistory') || store.get('sessionIsProcessing');
233
235
  handoff.disabled = unavailable;
234
236
  handoff.title = unavailable ? "Available after the current turn is complete" : "";
@@ -262,7 +262,7 @@ export function renderSidebarPresence(onlineUsers) {
262
262
  // Presence avatar URL helper
263
263
  function presenceAvatarUrl(userOrStyle) {
264
264
  if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
265
- return userAvatarUrl({ avatarStyle: userOrStyle || "thumbs" }, 24);
265
+ return userAvatarUrl({ avatarStyle: userOrStyle || "imprint" }, 24);
266
266
  }
267
267
 
268
268
  // renderUserStrip: call with no args to read from store (subscriber pattern),
@@ -424,7 +424,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
424
424
  avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
425
425
  avatar.src = mateAvatarUrl(mate, 34);
426
426
  avatar.alt = mp.displayName || mate.name || "Mate";
427
- var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
427
+ var mateColor = (mp.avatarColor) || mate.avatarColor || "#5857fc";
428
428
  avatar.style.background = mateColor + "30";
429
429
  el.appendChild(avatar);
430
430
 
@@ -679,7 +679,7 @@ function toggleDmUserPicker(anchorEl) {
679
679
  bItem.style.opacity = "0.7";
680
680
  var bAv = document.createElement("img");
681
681
  bAv.className = "dm-user-picker-avatar";
682
- bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
682
+ bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "imprint", avatarSeed: b.displayName, id: b.key, avatarColor: b.avatarColor }, 28);
683
683
  bAv.alt = b.displayName;
684
684
  bItem.appendChild(bAv);
685
685
  var bNameWrap = document.createElement("div");
@@ -866,7 +866,8 @@ function toggleDmUserPicker(anchorEl) {
866
866
  } else {
867
867
  avEl.src = mateAvatarUrl({
868
868
  avatarCustom: src.avatarCustom,
869
- avatarStyle: src.avatarStyle || "bottts",
869
+ avatarStyle: src.avatarStyle || "imprint",
870
+ avatarColor: src.avatarColor,
870
871
  avatarSeed: src.displayName,
871
872
  id: src.key,
872
873
  }, 32);
@@ -1543,7 +1543,7 @@ export function updateSessionPresence(presence) {
1543
1543
 
1544
1544
  function presenceAvatarUrl(userOrStyle, seed) {
1545
1545
  if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
1546
- return avatarUrl(userOrStyle || "thumbs", seed, 24);
1546
+ return avatarUrl(userOrStyle || "imprint", seed, 24);
1547
1547
  }
1548
1548
 
1549
1549
  function renderPresenceAvatars(el, sessionId) {
@@ -38,3 +38,4 @@
38
38
  @import url("css/notifications-center.css");
39
39
  @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
40
40
  @import url("css/pwa-mobile.css?v=20260824f");
41
+ @import url("css/avatar-imprints.css?v=20260829-imprints1");
@@ -414,8 +414,8 @@ function attachAuth(ctx) {
414
414
  adminProfile = {
415
415
  name: data.displayName || data.username,
416
416
  lang: existingProfile.lang || "en-US",
417
- avatarColor: existingProfile.avatarColor || "#7c3aed",
418
- avatarStyle: existingProfile.avatarStyle || "thumbs",
417
+ avatarColor: existingProfile.avatarColor || "#5857fc",
418
+ avatarStyle: existingProfile.avatarStyle || "imprint",
419
419
  avatarSeed: existingProfile.avatarSeed || crypto.randomBytes(4).toString("hex"),
420
420
  };
421
421
  } catch (e) {}
package/lib/server-dm.js CHANGED
@@ -28,9 +28,9 @@ function attachDm(ctx) {
28
28
  id: otherUser.id,
29
29
  displayName: p.name || otherUser.displayName || otherUser.username,
30
30
  username: otherUser.username,
31
- avatarStyle: p.avatarStyle || "thumbs",
31
+ avatarStyle: p.avatarStyle || "imprint",
32
32
  avatarSeed: p.avatarSeed || otherUser.username,
33
- avatarColor: p.avatarColor || "#7c3aed",
33
+ avatarColor: p.avatarColor || "#5857fc",
34
34
  avatarCustom: p.avatarCustom || "",
35
35
  };
36
36
  }
@@ -69,9 +69,9 @@ function attachDm(ctx) {
69
69
  id: mate.id,
70
70
  displayName: mp.displayName || mate.name || "New Mate",
71
71
  username: mate.id,
72
- avatarStyle: mp.avatarStyle || "bottts",
72
+ avatarStyle: mp.avatarStyle || "imprint",
73
73
  avatarSeed: mp.avatarSeed || mate.id,
74
- avatarColor: mp.avatarColor || "#6c5ce7",
74
+ avatarColor: mp.avatarColor || "#5857fc",
75
75
  avatarCustom: mp.avatarCustom || "",
76
76
  isMate: true,
77
77
  primary: !!mate.primary,
@@ -94,9 +94,9 @@ function attachDm(ctx) {
94
94
  id: targetUser.id,
95
95
  displayName: tp.name || targetUser.displayName || targetUser.username,
96
96
  username: targetUser.username,
97
- avatarStyle: tp.avatarStyle || "thumbs",
97
+ avatarStyle: tp.avatarStyle || "imprint",
98
98
  avatarSeed: tp.avatarSeed || targetUser.username,
99
- avatarColor: tp.avatarColor || "#7c3aed",
99
+ avatarColor: tp.avatarColor || "#5857fc",
100
100
  avatarCustom: tp.avatarCustom || "",
101
101
  } : null,
102
102
  }));
@@ -191,9 +191,9 @@ function attachDm(ctx) {
191
191
  displayName: p.name || u.displayName || u.username,
192
192
  username: u.username,
193
193
  role: u.role,
194
- avatarStyle: p.avatarStyle || "thumbs",
194
+ avatarStyle: p.avatarStyle || "imprint",
195
195
  avatarSeed: p.avatarSeed || u.username,
196
- avatarColor: p.avatarColor || "#7c3aed",
196
+ avatarColor: p.avatarColor || "#5857fc",
197
197
  avatarCustom: p.avatarCustom || "",
198
198
  };
199
199
  });
@@ -152,7 +152,7 @@ function attachMates(ctx) {
152
152
  displayName: bDef2.displayName,
153
153
  bio: bDef2.bio,
154
154
  avatarCustom: bDef2.avatarCustom || "",
155
- avatarStyle: bDef2.avatarStyle || "bottts",
155
+ avatarStyle: bDef2.avatarStyle || "imprint",
156
156
  avatarColor: bDef2.avatarColor || "",
157
157
  });
158
158
  }
@@ -186,7 +186,7 @@ function attachMates(ctx) {
186
186
  displayName: bDef3.displayName,
187
187
  bio: bDef3.bio,
188
188
  avatarCustom: bDef3.avatarCustom || "",
189
- avatarStyle: bDef3.avatarStyle || "bottts",
189
+ avatarStyle: bDef3.avatarStyle || "imprint",
190
190
  avatarColor: bDef3.avatarColor || "",
191
191
  });
192
192
  }
@@ -227,7 +227,7 @@ function attachMates(ctx) {
227
227
  for (var abkR = 0; abkR < missingKeysR.length; abkR++) {
228
228
  var bDefR = builtinDefsR.getBuiltinByKey(missingKeysR[abkR]);
229
229
  if (bDefR) {
230
- availableBuiltinsR.push({ key: bDefR.key, displayName: bDefR.displayName, bio: bDefR.bio, avatarCustom: bDefR.avatarCustom || "", avatarStyle: bDefR.avatarStyle || "bottts", avatarColor: bDefR.avatarColor || "" });
230
+ availableBuiltinsR.push({ key: bDefR.key, displayName: bDefR.displayName, bio: bDefR.bio, avatarCustom: bDefR.avatarCustom || "", avatarStyle: bDefR.avatarStyle || "imprint", avatarColor: bDefR.avatarColor || "" });
231
231
  }
232
232
  }
233
233
  ws.send(JSON.stringify({ type: "mate_created", mate: newMate, projectSlug: readdSlug, availableBuiltins: availableBuiltinsR, dmFavorites: updatedFavsR }));
@@ -21,7 +21,7 @@ function attachSettings(ctx) {
21
21
  res.end('{"error":"unauthorized"}');
22
22
  return true;
23
23
  }
24
- var profile = mu.profile || { name: "", lang: "en-US", avatarColor: "#7c3aed", avatarStyle: "thumbs", avatarSeed: "", avatarCustom: "" };
24
+ var profile = mu.profile || { name: "", lang: "en-US", avatarColor: "#5857fc", avatarStyle: "imprint", avatarSeed: "", avatarCustom: "" };
25
25
  profile.username = mu.username;
26
26
  profile.userId = mu.id;
27
27
  profile.role = mu.role;
@@ -35,7 +35,7 @@ function attachSettings(ctx) {
35
35
  res.end(JSON.stringify(profile));
36
36
  return true;
37
37
  }
38
- var profile = { name: "", lang: "en-US", avatarColor: "#7c3aed", avatarStyle: "thumbs", avatarSeed: "", avatarCustom: "" };
38
+ var profile = { name: "", lang: "en-US", avatarColor: "#5857fc", avatarStyle: "imprint", avatarSeed: "", avatarCustom: "" };
39
39
  try {
40
40
  var raw = fs.readFileSync(profilePath, "utf8");
41
41
  var saved = JSON.parse(raw);